mirror of
https://github.com/dmunozv04/iSponsorBlockTV.git
synced 2025-12-07 12:26:45 +03:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c196e76205 | ||
|
|
863ec5e163 | ||
|
|
128e1f72cb | ||
|
|
fede94e973 | ||
|
|
0d62f69460 | ||
|
|
8d76bdd1c1 | ||
|
|
8c7c2cc206 | ||
|
|
7a0a264caa | ||
|
|
b1f1bd1851 | ||
|
|
ab3285048d | ||
|
|
799e0a6f77 | ||
|
|
c95ab4897d | ||
|
|
23e90caefc | ||
|
|
7b6f9bd8a0 | ||
|
|
f3a2c82a56 | ||
|
|
d0506304fd | ||
|
|
a01994e2b0 | ||
|
|
bc2c4727dc | ||
|
|
9ebd39d491 | ||
|
|
2e6a0af8ce | ||
|
|
4aa5b1e08c | ||
|
|
2cc8fa128f | ||
|
|
4ab49ea61e | ||
|
|
3d8fa562b4 | ||
|
|
20c6870e4c | ||
|
|
b90a5e317c | ||
|
|
93e3fd5720 | ||
|
|
b88d2b61be | ||
|
|
5a5ebcfeb7 | ||
|
|
7a66d1acd5 | ||
|
|
de8d03285f | ||
|
|
d850ec8162 | ||
|
|
81f5b6568d | ||
|
|
28b6ac3218 | ||
|
|
138d8bd51c | ||
|
|
db647362c6 | ||
|
|
58ee703501 | ||
|
|
4f4d990544 | ||
|
|
6e1ee572d5 | ||
|
|
df118c967d | ||
|
|
262e3c606d | ||
|
|
d72b24c95b | ||
|
|
88f5e5d4bf | ||
|
|
aa26e8fb04 | ||
|
|
3bbabdb26e | ||
|
|
064bdcd93c | ||
|
|
50b71d9f5c | ||
|
|
9461d6516f | ||
|
|
f69d6d04cf | ||
|
|
ace8f3564f | ||
|
|
70cba12efa | ||
|
|
b35d9fd60e | ||
|
|
518b1d9b2e | ||
|
|
4460aaf35c | ||
|
|
d3260d17f1 | ||
|
|
ac6f15042c | ||
|
|
7e45f623f7 | ||
|
|
cece0242c4 | ||
|
|
b069487bd6 | ||
|
|
d4f9380eff | ||
|
|
245300d064 | ||
|
|
0bcb70979f | ||
|
|
b9e010af9b | ||
|
|
bd0deec85e |
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
|
||||||
37
.github/workflows/release_pypi.yml
vendored
Normal file
37
.github/workflows/release_pypi.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# This workflow will upload a Python Package using Twine when a release is created
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||||
|
|
||||||
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
|
# They are provided by a third-party and are governed by
|
||||||
|
# separate terms of service, privacy policy, and support
|
||||||
|
# documentation.
|
||||||
|
|
||||||
|
name: Upload Python Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install build wheel
|
||||||
|
- name: Build package
|
||||||
|
run: python -m build
|
||||||
|
- name: Publish package
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
7
.github/workflows/update_docker_readme.yml
vendored
7
.github/workflows/update_docker_readme.yml
vendored
@@ -18,12 +18,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Get the repository's code
|
# Get the repository's code
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Update description
|
# Update description
|
||||||
- name: Update repo description
|
- name: Update repo description
|
||||||
uses: peter-evans/dockerhub-description@v2
|
uses: peter-evans/dockerhub-description@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
repository: dmunozv04/isponsorblocktv
|
repository: dmunozv04/isponsorblocktv
|
||||||
|
short-description: ${{ github.event.repository.description }}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM python:alpine3.11
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
ENV PIP_NO_CACHE_DIR=off iSPBTV_docker=True TERM=xterm-256color COLORTERM=truecolor
|
ENV PIP_NO_CACHE_DIR=off iSPBTV_docker=True iSPBTV_data_dir=data TERM=xterm-256color COLORTERM=truecolor
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
RUN pip install --upgrade pip wheel && \
|
RUN pip install --upgrade pip wheel && \
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
RUN python -m compileall
|
||||||
|
|
||||||
|
COPY src .
|
||||||
|
|
||||||
ENTRYPOINT ["python3", "-u", "main.py"]
|
ENTRYPOINT ["python3", "-u", "main.py"]
|
||||||
17
README.md
17
README.md
@@ -9,7 +9,7 @@ Check the [wiki](https://github.com/dmunozv04/iSponsorBlockTV/wiki/Installation)
|
|||||||
Warning: docker armv7 builds have been deprecated. Amd64 and arm64 builds are still available.
|
Warning: docker armv7 builds have been deprecated. Amd64 and arm64 builds are still available.
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
Leyend: ✅ = Working, ❌ = Not working, ❔ = Not tested
|
Legend: ✅ = Working, ❌ = Not working, ❔ = Not tested
|
||||||
|
|
||||||
Open an issue/pull request if you have tested a device that isn't listed here.
|
Open an issue/pull request if you have tested a device that isn't listed here.
|
||||||
|
|
||||||
@@ -18,17 +18,20 @@ Open an issue/pull request if you have tested a device that isn't listed here.
|
|||||||
| Apple TV | ✅ |
|
| Apple TV | ✅ |
|
||||||
| Samsung TV (Tizen) | ✅ |
|
| Samsung TV (Tizen) | ✅ |
|
||||||
| LG TV (WebOS) | ✅ |
|
| LG TV (WebOS) | ✅ |
|
||||||
| Android TV | ❔ |
|
| Android TV | ✅ |
|
||||||
| Chromecast | ❔ |
|
| Chromecast | ✅ |
|
||||||
| Roku | ❔ |
|
| Google TV | ✅ |
|
||||||
| Fire TV | ❔ |
|
| Roku | ✅ |
|
||||||
|
| Fire TV | ✅ |
|
||||||
|
| 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.
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
from iSponsorBlockTV import helpers
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
if getattr(sys, "frozen", False):
|
|
||||||
os.environ["SSL_CERT_FILE"] = os.path.join(sys._MEIPASS, "lib", "cert.pem")
|
|
||||||
helpers.app_start()
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
|
||||||
|
|
||||||
from PyInstaller.utils.hooks import exec_statement
|
|
||||||
cert_datas = exec_statement("""
|
|
||||||
import ssl
|
|
||||||
print(ssl.get_default_verify_paths().cafile)""").strip().split()
|
|
||||||
cert_datas = [(f, 'lib') for f in cert_datas]
|
|
||||||
|
|
||||||
block_cipher = None
|
|
||||||
|
|
||||||
options = [ ('u', None, 'OPTION') ]
|
|
||||||
|
|
||||||
a = Analysis(['main-macos.py'],
|
|
||||||
pathex=[],
|
|
||||||
binaries=[],
|
|
||||||
datas=cert_datas,
|
|
||||||
hiddenimports=['certifi'],
|
|
||||||
hookspath=[],
|
|
||||||
hooksconfig={},
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=[],
|
|
||||||
win_no_prefer_redirects=False,
|
|
||||||
win_private_assemblies=False,
|
|
||||||
cipher=block_cipher,
|
|
||||||
noarchive=False)
|
|
||||||
pyz = PYZ(a.pure, a.zipped_data,
|
|
||||||
cipher=block_cipher)
|
|
||||||
|
|
||||||
exe = EXE(pyz,
|
|
||||||
a.scripts,
|
|
||||||
a.binaries,
|
|
||||||
a.zipfiles,
|
|
||||||
a.datas,
|
|
||||||
[],
|
|
||||||
options,
|
|
||||||
name='iSponsorBlockTV-macos',
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
upx_exclude=[],
|
|
||||||
runtime_tmpdir=None,
|
|
||||||
console=True,
|
|
||||||
disable_windowed_traceback=False,
|
|
||||||
target_arch=None,
|
|
||||||
codesign_identity=None,
|
|
||||||
entitlements_file=None)
|
|
||||||
30
pyproject.toml
Normal file
30
pyproject.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[project]
|
||||||
|
name = "iSponsorBlockTV"
|
||||||
|
version = "2.0.3"
|
||||||
|
authors = [
|
||||||
|
{"name" = "dmunozv04"}
|
||||||
|
]
|
||||||
|
description = "SponsorBlock client for all YouTube TV clients"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.7"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Topic :: Home Automation"
|
||||||
|
]
|
||||||
|
dynamic = ["dependencies"]
|
||||||
|
|
||||||
|
[tool.hatch.metadata.hooks.requirements_txt]
|
||||||
|
files = ["requirements.txt"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://github.com/dmunozv04/iSponsorBlockTV"
|
||||||
|
"Bug Tracker" = "https://github.com/dmunozv04/iSponsorBlockTV/issues"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
"iSponsorBlockTV" = "iSponsorBlockTV.__main__:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling", "hatch-requirements-txt"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
aiohttp==3.8.6
|
aiohttp==3.9.0
|
||||||
|
appdirs==1.4.4
|
||||||
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
|
||||||
textual-slider==0.1.1
|
textual-slider==0.1.1
|
||||||
xmltodict==0.13.0
|
xmltodict==0.13.0
|
||||||
|
|
||||||
|
|||||||
8
src/iSponsorBlockTV/__main__.py
Normal file
8
src/iSponsorBlockTV/__main__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from . import helpers
|
||||||
|
|
||||||
|
def main():
|
||||||
|
helpers.app_start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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:
|
||||||
@@ -138,7 +147,7 @@ class ApiHelper:
|
|||||||
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 appart, combine them and skip them together
|
||||||
segment_dict["start"] = segment_before_start
|
segment_dict["start"] = segment_before_start
|
||||||
segment_dict["UUID"].append(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:
|
||||||
@@ -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/"
|
||||||
@@ -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:
|
||||||
@@ -24,6 +23,7 @@ async def pair_device(loop):
|
|||||||
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:
|
||||||
@@ -61,30 +63,35 @@ def main(config, debug: bool) -> None:
|
|||||||
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:
|
||||||
@@ -118,6 +125,7 @@ def main(config, debug: bool) -> None:
|
|||||||
|
|
||||||
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()
|
||||||
@@ -17,3 +17,8 @@ skip_categories = (
|
|||||||
('Preview', 'preview'),
|
('Preview', 'preview'),
|
||||||
('Filler', 'filler'),
|
('Filler', 'filler'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
youtube_client_blacklist = ["TVHTML5_FOR_KIDS"]
|
||||||
|
|
||||||
|
|
||||||
|
config_file_blacklist_keys = ["config_file", "data_dir"]
|
||||||
@@ -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}
|
||||||
@@ -5,7 +5,10 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from appdirs import user_data_dir
|
||||||
|
|
||||||
from . import config_setup, main, setup_wizard
|
from . import config_setup, main, setup_wizard
|
||||||
|
from .constants import config_file_blacklist_keys
|
||||||
|
|
||||||
|
|
||||||
class Device:
|
class Device:
|
||||||
@@ -43,7 +46,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)
|
||||||
@@ -65,7 +69,8 @@ class Config:
|
|||||||
with open(self.config_file, "r") as f:
|
with open(self.config_file, "r") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
for i in config:
|
for i in config:
|
||||||
setattr(self, i, config[i])
|
if i not in config_file_blacklist_keys:
|
||||||
|
setattr(self, i, config[i])
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("Could not load config file")
|
print("Could not load config file")
|
||||||
# Create data directory if it doesn't exist (if we're not running in docker)
|
# Create data directory if it doesn't exist (if we're not running in docker)
|
||||||
@@ -90,9 +95,12 @@ class Config:
|
|||||||
config_dict = self.__dict__
|
config_dict = self.__dict__
|
||||||
# Don't save the config file name
|
# Don't save the config file name
|
||||||
config_file = self.config_file
|
config_file = self.config_file
|
||||||
|
data_dir = self.data_dir
|
||||||
del config_dict["config_file"]
|
del config_dict["config_file"]
|
||||||
|
del config_dict["data_dir"]
|
||||||
json.dump(config_dict, f, indent=4)
|
json.dump(config_dict, f, indent=4)
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
|
self.data_dir = data_dir
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, Config):
|
if isinstance(other, Config):
|
||||||
@@ -101,8 +109,10 @@ class Config:
|
|||||||
|
|
||||||
|
|
||||||
def app_start():
|
def app_start():
|
||||||
|
#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")
|
||||||
parser = argparse.ArgumentParser(description="iSponsorblockTV")
|
parser = argparse.ArgumentParser(description="iSponsorblockTV")
|
||||||
parser.add_argument("--data-dir", "-d", default="data", help="data directory")
|
parser.add_argument("--data-dir", "-d", default=default_data_dir, help="data directory")
|
||||||
parser.add_argument("--setup", "-s", action="store_true", help="setup the program graphically")
|
parser.add_argument("--setup", "-s", action="store_true", help="setup the program graphically")
|
||||||
parser.add_argument("--setup-cli", "-sc", action="store_true", help="setup the program in the command line")
|
parser.add_argument("--setup-cli", "-sc", action="store_true", help="setup the program in the command line")
|
||||||
parser.add_argument("--debug", action="store_true", help="debug mode")
|
parser.add_argument("--debug", action="store_true", help="debug mode")
|
||||||
@@ -3,15 +3,16 @@ 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:
|
||||||
def __init__(self, api_helper, config, screen_id, offset):
|
def __init__(self, api_helper, config, device):
|
||||||
self.task: asyncio.Task = None
|
self.task: asyncio.Task = None
|
||||||
self.api_helper = api_helper
|
self.api_helper = api_helper
|
||||||
self.lounge_controller = ytlounge.YtLoungeApi(screen_id, config, api_helper)
|
self.lounge_controller = ytlounge.YtLoungeApi(
|
||||||
self.offset = offset
|
device.screen_id, config, api_helper)
|
||||||
|
self.offset = device.offset
|
||||||
|
self.name = device.name
|
||||||
self.cancelled = False
|
self.cancelled = False
|
||||||
|
|
||||||
# Ensures that we have a valid auth token
|
# Ensures that we have a valid auth token
|
||||||
@@ -40,7 +41,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)
|
||||||
@@ -49,17 +49,18 @@ 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()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
print(f"Connected to device {lounge_controller.screen_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
|
||||||
|
|
||||||
@@ -105,18 +106,17 @@ class DeviceListener:
|
|||||||
# 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, UUID):
|
||||||
await asyncio.sleep(time_to)
|
await asyncio.sleep(time_to)
|
||||||
asyncio.create_task(self.lounge_controller.seek_to(position))
|
await asyncio.gather(
|
||||||
asyncio.create_task(
|
self.lounge_controller.seek_to(position),
|
||||||
self.api_helper.mark_viewed_segments(UUID)
|
self.api_helper.mark_viewed_segments(UUID)
|
||||||
) # 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
|
||||||
|
|
||||||
|
|
||||||
@@ -136,13 +136,13 @@ def main(config, debug):
|
|||||||
web_session = aiohttp.ClientSession(loop=loop, connector=tcp_connector)
|
web_session = aiohttp.ClientSession(loop=loop, connector=tcp_connector)
|
||||||
api_helper = api_helpers.ApiHelper(config, web_session)
|
api_helper = api_helpers.ApiHelper(config, web_session)
|
||||||
for i in config.devices:
|
for i in config.devices:
|
||||||
device = DeviceListener(api_helper, config, i.screen_id, i.offset)
|
device = DeviceListener(api_helper, config, i)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
tasks.append(loop.create_task(device.loop()))
|
tasks.append(loop.create_task(device.loop()))
|
||||||
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:
|
||||||
@@ -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")
|
||||||
@@ -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,14 +68,18 @@ 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 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
|
elif self.skip_ads and data["isSkipEnabled"] == "true": # YouTube uses strings for booleans
|
||||||
|
print("Ad can be skipped, skipping")
|
||||||
|
create_task(self.skip_ad())
|
||||||
|
create_task(self.mute(False, override=True))
|
||||||
|
elif self.mute_ads: # Seen multiple other adStates, assuming they are all ads
|
||||||
print("Ad has started, muting")
|
print("Ad has started, muting")
|
||||||
create_task(self.mute(True, override=True))
|
create_task(self.mute(True, override=True))
|
||||||
# Manages volume, useful since YouTube wants to know the volume when unmuting (even if they already have it)
|
# Manages volume, useful since YouTube wants to know the volume when unmuting (even if they already have it)
|
||||||
@@ -78,10 +87,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":
|
||||||
@@ -90,15 +100,22 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
|||||||
if vid_id := data["contentVideoId"]:
|
if vid_id := data["contentVideoId"]:
|
||||||
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))
|
||||||
|
if self.mute_ads:
|
||||||
|
create_task(self.mute(True, override=True))
|
||||||
|
|
||||||
if data["isSkippable"] == "true": # YouTube uses strings for booleans
|
elif event_type == "loungeStatus":
|
||||||
if self.skip_ads:
|
data = args[0]
|
||||||
create_task(self.skip_ad())
|
devices = json.loads(data["devices"])
|
||||||
create_task(self.mute(False, override=True))
|
for device in devices:
|
||||||
elif self.mute_ads:
|
if device["type"] == "LOUNGE_SCREEN":
|
||||||
create_task(self.mute(True, override=True))
|
device_info = json.loads(device.get("deviceInfo", "{}"))
|
||||||
else:
|
if device_info.get("clientName", "") in youtube_client_blacklist:
|
||||||
super()._process_event(event_id, event_type, args)
|
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 +134,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
|
||||||
Reference in New Issue
Block a user