Compare commits

...

65 Commits

Author SHA1 Message Date
David
c196e76205 Merge pull request #113 from dmunozv04/Fix-#112
Fixes Will not start v2.0.3 #112
2023-12-09 19:16:55 +01:00
dmunozv04
863ec5e163 Fixes Will not start v2.0.3 #112 2023-12-09 19:08:20 +01:00
David
128e1f72cb Merge pull request #111 from dmunozv04:fix-config-file-issues
ensures data_dir isn't saved to disk
2023-12-07 14:27:18 +01:00
dmunozv04
fede94e973 ensures data_dir isn't saved to disk 2023-12-07 14:26:14 +01:00
David
0d62f69460 Merge pull request #110 from dmunozv04/fix-ad-skips
Attempt to fix ad skips
2023-12-07 14:20:51 +01:00
dmunozv04
8d76bdd1c1 attempt to fix ad skips 2023-12-04 11:18:12 +01:00
David
8c7c2cc206 Merge pull request #106 from dmunozv04/publish-pypi
Publish on pypi
2023-11-29 12:06:40 +01:00
dmunozv04
7a0a264caa Fix requirements 2023-11-29 12:02:26 +01:00
David
b1f1bd1851 Merge pull request #107 from dmunozv04:use-newer-docker-image
Use newer docker image 3.11-alpine
2023-11-29 12:00:58 +01:00
dmunozv04
ab3285048d Use newer docker image 3.11-alpine 2023-11-29 12:00:20 +01:00
dmunozv04
799e0a6f77 Mark chromecast as working #87 2023-11-29 11:49:13 +01:00
dmunozv04
c95ab4897d Merge branch 'main' of https://github.com/dmunozv04/iSponsorBlockTV into publish-pypi 2023-11-29 11:44:13 +01:00
dmunozv04
23e90caefc Sync with main (again) 2023-11-29 11:39:09 +01:00
dmunozv04
7b6f9bd8a0 Sync with main
Co-authored-by: tsia <github@tsia.de>
Co-authored-by: kot0dama <89980752+kot0dama@users.noreply.github.com>
Co-authored-by: boltgolt <boltgolt@gmail.com>
2023-11-29 11:33:29 +01:00
David
f3a2c82a56 Merge pull request #105 from dmunozv04/fix-mark-viewed-segments
Fix to ensure that skipped segments get reported to SponsorBlock
2023-11-29 10:22:21 +01:00
David
d0506304fd Merge branch 'main' into fix-mark-viewed-segments 2023-11-29 10:14:40 +01:00
David
a01994e2b0 Merge pull request #76 from tsia/patch-1
print device name from config
2023-11-29 10:05:34 +01:00
dmunozv04
bc2c4727dc Make deepsource happier 2023-11-29 10:04:40 +01:00
dmunozv04
9ebd39d491 Merge branch 'main' into pr/tsia/76 2023-11-29 09:54:45 +01:00
David
2e6a0af8ce Merge pull request #78 from boltgolt/misc-wizard-key-text
Clarification on API key requirement
2023-11-29 09:33:21 +01:00
David
4aa5b1e08c Merge pull request #102 from kot0dama/kot0dama-patch-wizard-labels
Fix muting/skipping ads labels mixup in setup_wizard.py
2023-11-29 09:30:25 +01:00
David
2cc8fa128f Merge pull request #104 from dmunozv04/dependabot/pip/aiohttp-3.9.0
Bump aiohttp from 3.8.6 to 3.9.0
2023-11-29 09:28:04 +01:00
dependabot[bot]
4ab49ea61e Bump aiohttp from 3.8.6 to 3.9.0
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.8.6 to 3.9.0.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.8.6...v3.9.0)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-28 00:51:31 +00:00
kot0dama
3d8fa562b4 Fix muting/skipping ads labels mixup in setup_wizard.py 2023-11-22 12:08:14 +09:00
dmunozv04
20c6870e4c fix branch 2023-11-17 11:53:04 +01:00
dmunozv04
b90a5e317c remove . 2023-11-17 11:50:50 +01:00
dmunozv04
93e3fd5720 fix dockerfile copy 2023-11-17 11:43:33 +01:00
dmunozv04
b88d2b61be prepare for pypi packaging 2023-11-17 11:40:04 +01:00
dmunozv04
5a5ebcfeb7 move directories and data path 2023-11-17 11:38:22 +01:00
dmunozv04
7a66d1acd5 Merge remote-tracking branch 'origin/main' 2023-11-10 11:36:42 +01:00
dmunozv04
de8d03285f Upgrade pyytlounge and fix #99 2023-11-10 11:36:26 +01:00
David
d850ec8162 Merge pull request #96 from cryingcavecat/chore/confirm-xbox-compatability
Confirm Xbox Compatability. Clarify TV Code.
2023-11-09 17:20:08 +01:00
Liam
81f5b6568d Confirm Xbox Compatability. Clarify TV Code 2023-11-06 12:51:45 +02:00
dmunozv04
28b6ac3218 General fixes to make deepsource happier 2023-11-05 17:56:51 +01:00
dmunozv04
138d8bd51c Log errors when gettting segments properly. Also forgot to remove a print 2023-11-05 17:38:10 +01:00
dmunozv04
db647362c6 Handle YouTube Kids (TVHTML5_FOR_KIDS)
Should fix #84
2023-11-05 17:36:39 +01:00
dmunozv04
58ee703501 Fix getting segments
(managed to break it last time)
2023-11-05 17:35:22 +01:00
dmunozv04
4f4d990544 Getting response is a method and not an attribute 2023-11-03 15:57:39 +01:00
dmunozv04
6e1ee572d5 Print video id when logging the error 2023-11-03 15:51:48 +01:00
dmunozv04
df118c967d Improve logging when an error occurs while getting segments 2023-11-03 15:50:39 +01:00
dmunozv04
262e3c606d Rollback attempt at fixing autoplay 2023-11-03 15:49:16 +01:00
David
d72b24c95b Merge pull request #86 from adripo/patch-1
Create .dockerignore
2023-10-23 18:17:06 +02:00
David
88f5e5d4bf Merge pull request #85 from bertybuttface/patch-1
Confirm FireTV compatibility (thanks to #83)
2023-10-23 18:16:16 +02:00
adripo
aa26e8fb04 Create .dockerignore 2023-10-23 15:27:37 +02:00
bertybuttface
3bbabdb26e Confirm FireTV compatibility (thanks to #83) 2023-10-23 13:02:17 +01:00
dmunozv04
064bdcd93c removed unnecesary prints 2023-10-20 11:10:45 +02:00
dmunozv04
50b71d9f5c Attempts to respect the user's autoplay choice 2023-10-20 10:54:13 +02:00
boltgolt
9461d6516f Clarification on API key requirement 2023-10-17 18:06:23 +02:00
tsia
f69d6d04cf print device name from config 2023-10-17 12:31:58 +02:00
dmunozv04
ace8f3564f Confirm Roku compatibility (thanks to #68) 2023-10-15 18:59:16 +02:00
David
70cba12efa Merge pull request #70 from outadoc/patch-1
chore: confirm Android TV compatibility
2023-10-15 18:56:45 +02:00
Baptiste Candellier
b35d9fd60e chore: confirm Android TV compatibility 2023-10-15 18:36:21 +02:00
dmunozv04
518b1d9b2e Fix to ensure that skipped segments get reported to SponsorBlock (if the option is enabled) 2023-10-15 13:59:36 +02:00
David
4460aaf35c Merge pull request #63 from JoshCooley/add-google-tv-status
Add Google TV and CCwGTV status to README
2023-10-14 13:51:34 +02:00
dmunozv04
d3260d17f1 Update README.md to reflect compatibility with Google TV devices and CCwGTV (ChromeCast with Google TV)
Fix readme sync to dockerhub
2023-10-14 13:50:00 +02:00
David
ac6f15042c Merge branch 'main' into add-google-tv-status 2023-10-14 13:47:38 +02:00
David
7e45f623f7 Update update_docker_readme.yml
Fix dockerhub description updater
2023-10-14 10:36:52 +02:00
David
cece0242c4 Merge pull request #64 from JoshCooley/fix-segment-uuid-concatenation
Fix segment UUID concatenation
2023-10-14 10:24:27 +02:00
David
b069487bd6 Merge pull request #62 from jtokoph/patch-1
Update Playstation Status in README.md
2023-10-14 10:01:48 +02:00
David
d4f9380eff Merge pull request #59 from nickgal/patch-1
README.md typo
2023-10-14 10:00:53 +02:00
Josh Cooley
245300d064 Fix segment UUID concatenation 2023-10-14 00:03:35 -05:00
Josh Cooley
0bcb70979f Add Google TV status to README 2023-10-13 22:42:51 -05:00
Jason Tokoph
b9e010af9b Update Playstation Status in README.md
I have tested this with the Youtube app on my Playstation 5 and can confirm that it correctly skips sponsorblock segments.
2023-10-13 20:17:04 -07:00
Nick Gal
bd0deec85e README.md typo 2023-10-13 17:54:14 -07:00
dmunozv04
d0846b7d2c Fix #58 2023-10-13 21:28:23 +02:00
25 changed files with 297 additions and 165 deletions

9
.dockerignore Normal file
View 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
View 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

View File

@@ -18,12 +18,13 @@ jobs:
steps:
# Get the repository's code
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
# Update description
- name: Update repo description
uses: peter-evans/dockerhub-description@v2
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: dmunozv04/isponsorblocktv
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: dmunozv04/isponsorblocktv
short-description: ${{ github.event.repository.description }}

View File

@@ -1,18 +1,19 @@
# 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 .
RUN pip install --upgrade pip wheel && \
pip install -r requirements.txt
COPY requirements.txt .
WORKDIR /app
COPY . .
RUN python -m compileall
COPY src .
ENTRYPOINT ["python3", "-u", "main.py"]

View File

@@ -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.
## 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.
@@ -18,17 +18,20 @@ Open an issue/pull request if you have tested a device that isn't listed here.
| Apple TV | ✅ |
| Samsung TV (Tizen) | ✅ |
| LG TV (WebOS) | ✅ |
| Android TV | |
| Chromecast | |
| Roku | |
| Fire TV | |
| Android TV | |
| Chromecast | |
| Google TV | |
| Roku | |
| Fire TV | ✅ |
| CCwGTV | ✅ |
| Nintendo Switch | ✅ |
| Xbox One/Series | |
| Playstation 4/5 | |
| 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.

View File

@@ -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()

View File

@@ -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
View 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"

View File

@@ -1,10 +1,10 @@
aiohttp==3.8.6
aiohttp==3.9.0
appdirs==1.4.4
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
textual-slider==0.1.1
xmltodict==0.13.0
xmltodict==0.13.0

View File

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

View File

@@ -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:
@@ -138,7 +147,7 @@ class ApiHelper:
segment_dict["start"] - segment_before_end < 1
): # Less than 1 second appart, combine them and skip them together
segment_dict["start"] = segment_before_start
segment_dict["UUID"].append(segment_before_UUID)
segment_dict["UUID"].extend(segment_before_UUID)
segments.pop()
segments.append(segment_dict)
except Exception:
@@ -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/"

View File

@@ -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:
@@ -23,7 +22,8 @@ async def pair_device(loop):
except Exception as e:
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:
@@ -56,41 +58,46 @@ def main(config, debug: bool) -> None:
apikey = input("Enter your API key: ")
config["apikey"] = apikey
config.apikey = apikey
skip_categories = config.skip_categories
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:
print("No channels found")
continue
for i in range(len(results)):
print(f"{i}: {results[i][1]} - Subs: {results[i][2]}")
print("5: Enter a custom channel ID")
@@ -115,9 +122,10 @@ def main(config, debug: bool) -> None:
channel_whitelist.append(channel_info)
# Close web session asynchronously
loop.run_until_complete(web_session.close())
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()

View File

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

View File

@@ -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}

View File

@@ -5,7 +5,10 @@ import os
import sys
import time
from appdirs import user_data_dir
from . import config_setup, main, setup_wizard
from .constants import config_file_blacklist_keys
class Device:
@@ -43,7 +46,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)
@@ -65,7 +69,8 @@ class Config:
with open(self.config_file, "r") as f:
config = json.load(f)
for i in config:
setattr(self, i, config[i])
if i not in config_file_blacklist_keys:
setattr(self, i, config[i])
except FileNotFoundError:
print("Could not load config file")
# 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__
# Don't save the config file name
config_file = self.config_file
data_dir = self.data_dir
del config_dict["config_file"]
del config_dict["data_dir"]
json.dump(config_dict, f, indent=4)
self.config_file = config_file
self.data_dir = data_dir
def __eq__(self, other):
if isinstance(other, Config):
@@ -101,8 +109,10 @@ class Config:
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.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-cli", "-sc", action="store_true", help="setup the program in the command line")
parser.add_argument("--debug", action="store_true", help="debug mode")

View File

@@ -3,15 +3,16 @@ import aiohttp
import time
import logging
from . import api_helpers, ytlounge
import traceback
class DeviceListener:
def __init__(self, api_helper, config, screen_id, offset):
def __init__(self, api_helper, config, device):
self.task: asyncio.Task = None
self.api_helper = api_helper
self.lounge_controller = ytlounge.YtLoungeApi(screen_id, config, api_helper)
self.offset = offset
self.lounge_controller = ytlounge.YtLoungeApi(
device.screen_id, config, api_helper)
self.offset = device.offset
self.name = device.name
self.cancelled = False
# Ensures that we have a valid auth token
@@ -40,7 +41,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)
@@ -49,17 +49,18 @@ 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()
except:
pass
print(f"Connected to device {lounge_controller.screen_name}")
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
@@ -105,18 +106,17 @@ class DeviceListener:
# Skips to the next segment (waits for the time to pass)
async def skip(self, time_to, position, UUID):
await asyncio.sleep(time_to)
asyncio.create_task(self.lounge_controller.seek_to(position))
asyncio.create_task(
await asyncio.gather(
self.lounge_controller.seek_to(position),
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
@@ -136,14 +136,14 @@ def main(config, debug):
web_session = aiohttp.ClientSession(loop=loop, connector=tcp_connector)
api_helper = api_helpers.ApiHelper(config, web_session)
for i in config.devices:
device = DeviceListener(api_helper, config, i.screen_id, i.offset)
device = DeviceListener(api_helper, config, i)
devices.append(device)
tasks.append(loop.create_task(device.loop()))
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:
loop.run_until_complete(web_session.close())
loop.run_until_complete(web_session.close())

View File

@@ -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
@@ -226,12 +228,12 @@ class AddDevice(ModalWithClickExit):
@on(Input.Changed, "#pairing-code-input")
def changed_pairing_code(self, event: Input.Changed):
self.query_one("#add-device-button").disabled = not event.validation_result.is_valid
self.query_one("#add-device-pin-add-button").disabled = not event.validation_result.is_valid
@on(Input.Submitted, "#pairing-code-input")
@on(Button.Pressed, "#add-device-pin-add-button")
async def handle_add_device_pin(self) -> None:
self.query_one("#add-device-button").disabled = True
self.query_one("#add-device-pin-add-button").disabled = True
lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV")
pairing_code = self.query_one("#pairing-code-input").value
pairing_code = int(pairing_code.replace("-", "").replace(" ", "")) # remove dashes and spaces
@@ -253,7 +255,7 @@ class AddDevice(ModalWithClickExit):
self.dismiss([device])
else:
self.query_one("#pairing-code-input").value = ""
self.query_one("#add-device-button").disabled = False
self.query_one("#add-device-pin-add-button").disabled = False
self.query_one("#add-device-info").update("[#ff0000]Failed to add device")
@on(Button.Pressed, "#add-device-dial-add-button")
@@ -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")

View File

@@ -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,14 +68,18 @@ 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":
elif 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
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")
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)
@@ -78,10 +87,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":
@@ -90,15 +100,22 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
if vid_id := data["contentVideoId"]:
print(f"Getting segments for next video: {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
if self.skip_ads:
create_task(self.skip_ad())
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 +134,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

View File

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