mirror of
https://github.com/dmunozv04/iSponsorBlockTV.git
synced 2025-12-06 20:06:44 +03:00
Compare commits
134 Commits
v2.0.8
...
debug-comm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce58552a89 | ||
|
|
f486fec0bd | ||
|
|
54015cf455 | ||
|
|
114326e34c | ||
|
|
3797200825 | ||
|
|
eafedb7cf7 | ||
|
|
1cf539be9a | ||
|
|
9d74a9b3ce | ||
|
|
9b5ea2b243 | ||
|
|
112a4faa50 | ||
|
|
027a8d7ebc | ||
|
|
a495cdf62e | ||
|
|
24f1612f20 | ||
|
|
c979d280a1 | ||
|
|
45bc7ff6e7 | ||
|
|
251a94f147 | ||
|
|
0ba8f4c3c5 | ||
|
|
2ebc821ed9 | ||
|
|
060fe7af5d | ||
|
|
879116e873 | ||
|
|
1914afa432 | ||
|
|
82459c8986 | ||
|
|
c7cb4e8282 | ||
|
|
7ea0b8642c | ||
|
|
79fc9c066c | ||
|
|
2b9e1f8bf4 | ||
|
|
f5b183a679 | ||
|
|
bececa5096 | ||
|
|
4589c612d1 | ||
|
|
26d1d7e481 | ||
|
|
73b277706e | ||
|
|
5884844da1 | ||
|
|
38efe843de | ||
|
|
ccfcd00aa9 | ||
|
|
e34dbb0f80 | ||
|
|
ae6da834e4 | ||
|
|
7e80b41bbb | ||
|
|
3c66dc3607 | ||
|
|
0c8427edc0 | ||
|
|
e637e514b2 | ||
|
|
c8b9fe157b | ||
|
|
dca9186d8b | ||
|
|
adc7e1efe9 | ||
|
|
b96014840a | ||
|
|
147004e257 | ||
|
|
6f4c27c0a5 | ||
|
|
f75dff8faf | ||
|
|
0e77418c2b | ||
|
|
f1d1787511 | ||
|
|
49b1b902d3 | ||
|
|
5116e6c1e0 | ||
|
|
0b0a235046 | ||
|
|
4eaebd3006 | ||
|
|
58e10f0f82 | ||
|
|
a93eeaa1cf | ||
|
|
77527ce4d5 | ||
|
|
6825ac6629 | ||
|
|
b59135316e | ||
|
|
7b17d5a2da | ||
|
|
7e9f53e175 | ||
|
|
84c22c2dde | ||
|
|
a7fbcd3dd2 | ||
|
|
dbf5e3ac1c | ||
|
|
23f65125e2 | ||
|
|
4feecaa570 | ||
|
|
52a3f238d6 | ||
|
|
afff2a44b9 | ||
|
|
15c165d89a | ||
|
|
e71598599a | ||
|
|
760970a751 | ||
|
|
fb598809da | ||
|
|
35d13373f9 | ||
|
|
0145b3ba8d | ||
|
|
8fdd13da04 | ||
|
|
6dcc12baed | ||
|
|
afaced8f84 | ||
|
|
b3dd27748b | ||
|
|
73192a1171 | ||
|
|
c7dc54fd66 | ||
|
|
edca2bf11a | ||
|
|
f3d3bdd432 | ||
|
|
67eb40ca9d | ||
|
|
284b7a1d2f | ||
|
|
ec109e0f10 | ||
|
|
658fad64eb | ||
|
|
7880e222b0 | ||
|
|
dcf53dcca9 | ||
|
|
1a271c567a | ||
|
|
4473fc925e | ||
|
|
d9ab2cd070 | ||
|
|
ea2004ba94 | ||
|
|
63f5a3bc41 | ||
|
|
e999a93503 | ||
|
|
8cc3f8aa05 | ||
|
|
5fadc81a69 | ||
|
|
39aef5babf | ||
|
|
c56cbfe095 | ||
|
|
bde4ecb72f | ||
|
|
167383dea8 | ||
|
|
fb3c40d477 | ||
|
|
cb738965a7 | ||
|
|
9ad335793a | ||
|
|
fd400d077a | ||
|
|
f9c7b58ece | ||
|
|
464baa7c59 | ||
|
|
d9986e52b3 | ||
|
|
547a47b9ec | ||
|
|
87d0e0e32e | ||
|
|
854cb2462f | ||
|
|
662b71fc00 | ||
|
|
fd6b7cb43a | ||
|
|
d17e59bf0d | ||
|
|
5bc6382f89 | ||
|
|
205191f442 | ||
|
|
810cd5eec3 | ||
|
|
e2ace8629f | ||
|
|
e54ead26d2 | ||
|
|
49fba2f28f | ||
|
|
b1333a2f61 | ||
|
|
cfef219d32 | ||
|
|
338e0479ba | ||
|
|
bfefa94a7b | ||
|
|
783e3d4240 | ||
|
|
015f5a79c9 | ||
|
|
dc72db0609 | ||
|
|
8de38cc92b | ||
|
|
94ba642af1 | ||
|
|
d3341009a6 | ||
|
|
5dbd16ddd5 | ||
|
|
faa0379b89 | ||
|
|
fb3ed6b39a | ||
|
|
865f5469a2 | ||
|
|
daa7026221 | ||
|
|
4de056d2a8 |
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,12 +7,14 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before opening an issue make sure that there are no duplicates and that you are on the latest version.
|
||||
Before opening an issue make sure that there are no duplicates and that you are
|
||||
on the latest version.
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@@ -25,13 +27,14 @@ A clear and concise description of what you expected to happen.
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**iSponsorBlockTV server (please complete the following information):**
|
||||
- OS: [e.g. Docker on linux Arm64, windows]
|
||||
- Python version [e.g. 3.7] (no need to fill if running on docker
|
||||
|
||||
- OS: [e.g. Docker on linux Arm64, windows]
|
||||
- Python version [e.g. 3.7] (no need to fill if running on docker
|
||||
|
||||
**Apple TV (please complete the following information):**
|
||||
- Device: [e.g. Apple TV 4]
|
||||
- OS: [e.g. tvOS 15.4]
|
||||
|
||||
- Device: [e.g. Apple TV 4]
|
||||
- OS: [e.g. tvOS 15.4]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -8,13 +8,15 @@ assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
A clear and concise description of any alternative solutions or features you've
|
||||
considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
25
.github/workflows/build_docker_images.yml
vendored
25
.github/workflows/build_docker_images.yml
vendored
@@ -21,18 +21,22 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Get the repository's code
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Generate docker tags
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/dmunozv04/isponsorblocktv, dmunozv04/isponsorblocktv
|
||||
images: |
|
||||
ghcr.io/${{ github.repository }}
|
||||
${{ env.DOCKERHUB_USERNAME && 'dmunozv04/isponsorblocktv' || '' }}
|
||||
tags: |
|
||||
type=raw,value=develop,priority=900,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
type=ref,enable=true,priority=600,prefix=pr-,suffix=,event=pr
|
||||
@@ -42,35 +46,34 @@ jobs:
|
||||
|
||||
# https://github.com/docker/setup-qemu-action
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm64, linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache
|
||||
# Only cache if it's not a pull request
|
||||
cache-to: ${{ github.event_name != 'pull_request' && 'type=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache,mode=max' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
205
.github/workflows/release.yml
vendored
Normal file
205
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
# 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: Release Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.11"
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build-sdist-and-wheel:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install Hatch
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install hatch
|
||||
|
||||
- name: Build package
|
||||
run: python -m hatch build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sdist-and-wheel
|
||||
path: dist/*
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
build-binaries:
|
||||
name: Build binaries for ${{ matrix.job.release_suffix }} (${{ matrix.job.os }})
|
||||
needs:
|
||||
- build-sdist-and-wheel
|
||||
runs-on: ${{ matrix.job.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
# Linux
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
cross: true
|
||||
release_suffix: x86_64-linux
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
cross: true
|
||||
cpu_variant: v1
|
||||
release_suffix: x86_64-linux-v1
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-latest
|
||||
cross: true
|
||||
release_suffix: aarch64-linux
|
||||
# Windows
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
release_suffix: x86_64-windows
|
||||
# macOS
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
release_suffix: aarch64-osx
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
cross: true
|
||||
release_suffix: x86_64-osx
|
||||
|
||||
env:
|
||||
PYAPP_PASS_LOCATION: "1"
|
||||
PYAPP_UV_ENABLED: "1"
|
||||
HATCH_BUILD_LOCATION: dist
|
||||
CARGO: cargo
|
||||
CARGO_BUILD_TARGET: ${{ matrix.job.target }}
|
||||
PYAPP_DISTRIBUTION_VARIANT_CPU: ${{ matrix.job.cpu_variant }}
|
||||
PYAPP_REPO: pyapp # Use local copy of pyapp (needed for cross-compiling)
|
||||
PYAPP_VERSION: v0.24.0
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Clone PyApp
|
||||
run: git clone --depth 1 --branch $PYAPP_VERSION https://github.com/ofek/pyapp $PYAPP_REPO
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install Hatch
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install hatch
|
||||
|
||||
- name: Install Rust toolchain
|
||||
if: ${{ !matrix.job.cross }}
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.job.target }}
|
||||
|
||||
- name: Set up cross compiling tools
|
||||
if: matrix.job.cross
|
||||
uses: taiki-e/setup-cross-toolchain-action@v1
|
||||
with:
|
||||
target: ${{ matrix.job.target}}
|
||||
|
||||
- name: Show toolchain information
|
||||
run: |-
|
||||
rustup toolchain list
|
||||
rustup default
|
||||
rustup -V
|
||||
rustc -V
|
||||
cargo -V
|
||||
hatch --version
|
||||
|
||||
- name: Get artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sdist-and-wheel
|
||||
path: ${{ github.workspace }}/dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Build Binary
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |-
|
||||
current_version=$(hatch version)
|
||||
PYAPP_PROJECT_PATH="${{ github.workspace }}/dist/isponsorblocktv-${current_version}-py3-none-any.whl" hatch -v build -t binary
|
||||
|
||||
- name: Rename binary
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |-
|
||||
mv dist/binary/iSponsorBlockTV* dist/binary/iSponsorBlockTV-${{ matrix.job.release_suffix }}
|
||||
|
||||
- name: Upload built binary package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries-${{ matrix.job.release_suffix }}
|
||||
path: dist/binary/*
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
publish-to-pypi:
|
||||
needs: build-sdist-and-wheel
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
# only run step if the event is a published release
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: sdist-and-wheel
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
|
||||
publish-to-release:
|
||||
permissions:
|
||||
contents: write
|
||||
needs:
|
||||
- build-sdist-and-wheel
|
||||
- build-binaries
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Get artifact
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Add assets to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/*
|
||||
37
.github/workflows/release_pypi.yml
vendored
37
.github/workflows/release_pypi.yml
vendored
@@ -1,37 +0,0 @@
|
||||
# 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
|
||||
2
.github/workflows/update_docker_readme.yml
vendored
2
.github/workflows/update_docker_readme.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
# Update description
|
||||
- name: Update repo description
|
||||
uses: peter-evans/dockerhub-description@v3
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
1
.markdownlintignore
Normal file
1
.markdownlintignore
Normal file
@@ -0,0 +1 @@
|
||||
LICENSE.md
|
||||
@@ -3,7 +3,7 @@
|
||||
# Inspired by textual pre-commit config
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-ast # simply checks whether the files parse as valid python
|
||||
- id: check-builtin-literals # requires literal syntax when initializing empty or zero python builtin types
|
||||
@@ -18,22 +18,14 @@ repos:
|
||||
- id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline
|
||||
- id: mixed-line-ending # replaces or checks mixed line ending
|
||||
- id: trailing-whitespace # checks for trailing whitespace
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.6
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
language_version: '3.11'
|
||||
args: ["--profile", "black", "--filter-files"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
- id: ruff
|
||||
args: [ --fix, --exit-non-zero-on-fix ]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.44.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: '3.11'
|
||||
args: ["--preview"]
|
||||
- repo: https://github.com/hadialqattan/pycln # removes unused imports
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: pycln
|
||||
language_version: "3.11"
|
||||
args: [--all]
|
||||
- id: markdownlint
|
||||
args: ["--fix"]
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM python:3.11-alpine3.19 as BASE
|
||||
FROM python:3.13-alpine3.21 AS base
|
||||
|
||||
FROM base as compiler
|
||||
FROM base AS compiler
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -10,18 +10,18 @@ COPY src .
|
||||
RUN python3 -m compileall -b -f . && \
|
||||
find . -name "*.py" -type f -delete
|
||||
|
||||
FROM base as DEP_INSTALLER
|
||||
FROM base AS dep_installer
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN apk add --no-cache gcc musl-dev && \
|
||||
pip install --upgrade pip wheel && \
|
||||
pip install --user -r requirements.txt && \
|
||||
pip install -r requirements.txt && \
|
||||
pip uninstall -y pip wheel && \
|
||||
apk del gcc musl-dev && \
|
||||
python3 -m compileall -b -f /root/.local/lib/python3.11/site-packages && \
|
||||
find /root/.local/lib/python3.11/site-packages -name "*.py" -type f -delete && \
|
||||
find /root/.local/lib/python3.11/ -name "__pycache__" -type d -exec rm -rf {} +
|
||||
python3 -m compileall -b -f /usr/local/lib/python3.13/site-packages && \
|
||||
find /usr/local/lib/python3.13/site-packages -name "*.py" -type f -delete && \
|
||||
find /usr/local/lib/python3.13/ -name "__pycache__" -type d -exec rm -rf {} +
|
||||
|
||||
FROM base
|
||||
|
||||
@@ -29,7 +29,7 @@ ENV PIP_NO_CACHE_DIR=off iSPBTV_docker=True iSPBTV_data_dir=data TERM=xterm-256c
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
COPY --from=dep_installer /root/.local /root/.local
|
||||
COPY --from=dep_installer /usr/local /usr/local
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -657,7 +657,7 @@ notice like this when it starts in an interactive mode:
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
The hypothetical commands `show w' and`show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
|
||||
43
README.md
43
README.md
@@ -1,14 +1,21 @@
|
||||
# iSponsorBlockTV
|
||||
Skip sponsor segments in YouTube videos playing on a YouTube TV device (see below for compatibility details).
|
||||
|
||||
[](https://ghcr.io/dmunozv04/isponsorblocktv)
|
||||
[](https://hub.docker.com/r/dmunozv04/isponsorblocktv/)
|
||||
[](https://github.com/dmunozv04/iSponsorBlockTV/releases/latest)
|
||||
[](https://github.com/dmunozv04/isponsorblocktv)
|
||||
|
||||
Skip sponsor segments in YouTube videos playing on a YouTube TV device (see
|
||||
below for compatibility details).
|
||||
|
||||
This project is written in asynchronous python and should be pretty quick.
|
||||
|
||||
## Installation
|
||||
|
||||
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
|
||||
|
||||
Legend: ✅ = Working, ❌ = Not working, ❔ = Not tested
|
||||
|
||||
Open an issue/pull request if you have tested a device that isn't listed here.
|
||||
@@ -29,24 +36,33 @@ Open an issue/pull request if you have tested a device that isn't listed here.
|
||||
| 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.
|
||||
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.
|
||||
|
||||
## Libraries used
|
||||
- [pyytlounge](https://github.com/FabioGNR/pyytlounge) Used to interact with the device
|
||||
|
||||
- [pyytlounge](https://github.com/FabioGNR/pyytlounge) Used to interact with the
|
||||
device
|
||||
- asyncio and [aiohttp](https://github.com/aio-libs/aiohttp)
|
||||
- [async-cache](https://github.com/iamsinghrajat/async-cache)
|
||||
- [Textual](https://github.com/textualize/textual/) Used for the amazing new graphical configurator
|
||||
- [Textual](https://github.com/textualize/textual/) Used for the amazing new
|
||||
graphical configurator
|
||||
- [ssdp](https://github.com/codingjoe/ssdp) Used for auto discovery
|
||||
|
||||
## Projects using this project
|
||||
|
||||
- [Home Assistant Addon](https://github.com/bertybuttface/addons/tree/main/isponsorblocktv)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork it (<https://github.com/dmunozv04/iSponsorBlockTV/fork>)
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
@@ -54,8 +70,13 @@ It can also skip/mute YouTube ads.
|
||||
5. Create a new Pull Request
|
||||
|
||||
## Contributors
|
||||
|
||||
- [dmunozv04](https://github.com/dmunozv04) - creator and maintainer
|
||||
- [HaltCatchFire](https://github.com/HaltCatchFire) - updated dependencies and improved skip logic
|
||||
- [Oxixes](https://github.com/oxixes) - added support for channel whitelist and minor improvements
|
||||
- [HaltCatchFire](https://github.com/HaltCatchFire) - updated dependencies and
|
||||
improved skip logic
|
||||
- [Oxixes](https://github.com/oxixes) - added support for channel whitelist and
|
||||
minor improvements
|
||||
|
||||
## License
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"skip_count_tracking": true,
|
||||
"mute_ads": true,
|
||||
"skip_ads": true,
|
||||
"auto_play": true,
|
||||
"join_name": "iSponsorBlockTV",
|
||||
"apikey": "",
|
||||
"channel_whitelist": [
|
||||
{"id": "",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
[project]
|
||||
name = "iSponsorBlockTV"
|
||||
version = "2.0.8"
|
||||
version = "2.3.1"
|
||||
authors = [
|
||||
{"name" = "dmunozv04"}
|
||||
]
|
||||
description = "SponsorBlock client for all YouTube TV clients"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
requires-python = ">=3.9"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
aiohttp==3.9.5
|
||||
aiohttp==3.11.12
|
||||
appdirs==1.4.4
|
||||
argparse==1.4.0
|
||||
async-cache==1.1.1
|
||||
pyytlounge==2.0.0
|
||||
rich==13.7.1
|
||||
pyytlounge==2.1.2
|
||||
rich==13.9.4
|
||||
ssdp==1.3.0
|
||||
textual==0.58.0
|
||||
textual-slider==0.1.1
|
||||
xmltodict==0.13.0
|
||||
textual==1.0.0
|
||||
textual-slider==0.2.0
|
||||
xmltodict==0.14.2
|
||||
rich_click==1.8.5
|
||||
|
||||
@@ -1,169 +1,198 @@
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
|
||||
from . import api_helpers, ytlounge
|
||||
|
||||
|
||||
async def pair_device(web_session):
|
||||
try:
|
||||
lounge_controller = ytlounge.YtLoungeApi(
|
||||
"iSponsorBlockTV", web_session=web_session
|
||||
)
|
||||
pairing_code = input(
|
||||
"Enter pairing code (found in Settings - Link with TV code): "
|
||||
)
|
||||
pairing_code = int(
|
||||
pairing_code.replace("-", "").replace(" ", "")
|
||||
) # remove dashes and spaces
|
||||
print("Pairing...")
|
||||
paired = await lounge_controller.pair(pairing_code)
|
||||
if not paired:
|
||||
print("Failed to pair device")
|
||||
return
|
||||
device = {
|
||||
"screen_id": lounge_controller.auth.screen_id,
|
||||
"name": lounge_controller.screen_name,
|
||||
}
|
||||
print(f"Paired device: {device['name']}")
|
||||
return device
|
||||
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()
|
||||
web_session = aiohttp.ClientSession()
|
||||
if debug:
|
||||
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"
|
||||
)
|
||||
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(web_session))
|
||||
loop.run_until_complete(task)
|
||||
device = task.result()
|
||||
if device:
|
||||
devices.append(device)
|
||||
config.devices = devices
|
||||
|
||||
apikey = config.apikey
|
||||
if apikey:
|
||||
if input("API key already specified. Change it? (y/n) ") == "y":
|
||||
apikey = input("Enter your API key: ")
|
||||
else:
|
||||
if (
|
||||
input(
|
||||
"API key only needed for the channel whitelist function. Add it? (y/n) "
|
||||
)
|
||||
== "y"
|
||||
):
|
||||
print(
|
||||
"Get youtube apikey here:"
|
||||
" https://developers.google.com/youtube/registering_an_application"
|
||||
)
|
||||
apikey = input("Enter your API key: ")
|
||||
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"
|
||||
)
|
||||
skip_categories = categories.replace(",", " ").split(" ")
|
||||
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"
|
||||
)
|
||||
skip_categories = categories.replace(",", " ").split(" ")
|
||||
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."
|
||||
)
|
||||
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_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, item in enumerate(results):
|
||||
print(f"{i}: {item[1]} - Subs: {item[2]}")
|
||||
print("5: Enter a custom channel ID")
|
||||
print("6: Go back")
|
||||
|
||||
choice = -1
|
||||
choice = input("Select one option of the above [0-6]: ")
|
||||
while choice not in [str(x) for x in range(7)]:
|
||||
print("Invalid choice")
|
||||
choice = input("Select one option of the above [0-6]: ")
|
||||
|
||||
if choice == "5":
|
||||
channel_info["id"] = input("Enter a channel ID: ")
|
||||
channel_info["name"] = input("Enter the channel name: ")
|
||||
channel_whitelist.append(channel_info)
|
||||
continue
|
||||
if choice == "6":
|
||||
continue
|
||||
|
||||
channel_info["id"] = results[int(choice)][0]
|
||||
channel_info["name"] = results[int(choice)][1]
|
||||
channel_whitelist.append(channel_info)
|
||||
# Close web session asynchronously
|
||||
|
||||
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"
|
||||
)
|
||||
print("Config finished")
|
||||
config.save()
|
||||
loop.run_until_complete(web_session.close())
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
|
||||
from . import api_helpers, ytlounge
|
||||
|
||||
# Constants for user input prompts
|
||||
ATVS_REMOVAL_PROMPT = (
|
||||
"Do you want to remove the legacy 'atvs' entry (the app won't start"
|
||||
" with it present)? (y/N) "
|
||||
)
|
||||
PAIRING_CODE_PROMPT = "Enter pairing code (found in Settings - Link with TV code): "
|
||||
ADD_MORE_DEVICES_PROMPT = "Paired with {num_devices} Device(s). Add more? (y/N) "
|
||||
CHANGE_API_KEY_PROMPT = "API key already specified. Change it? (y/N) "
|
||||
ADD_API_KEY_PROMPT = (
|
||||
"API key only needed for the channel whitelist function. Add it? (y/N) "
|
||||
)
|
||||
ENTER_API_KEY_PROMPT = "Enter your API key: "
|
||||
CHANGE_SKIP_CATEGORIES_PROMPT = "Skip categories already specified. Change them? (y/N) "
|
||||
ENTER_SKIP_CATEGORIES_PROMPT = (
|
||||
"Enter skip categories (space or comma sepparated) Options: [sponsor,"
|
||||
" selfpromo, exclusive_access, interaction, poi_highlight, intro, outro,"
|
||||
" preview, filler, music_offtopic]:\n"
|
||||
)
|
||||
WHITELIST_CHANNELS_PROMPT = (
|
||||
"Do you want to whitelist any channels from being ad-blocked? (y/N) "
|
||||
)
|
||||
SEARCH_CHANNEL_PROMPT = 'Enter a channel name or "/exit" to exit: '
|
||||
SELECT_CHANNEL_PROMPT = "Select one option of the above [0-6]: "
|
||||
ENTER_CHANNEL_ID_PROMPT = "Enter a channel ID: "
|
||||
ENTER_CUSTOM_CHANNEL_NAME_PROMPT = "Enter the channel name: "
|
||||
REPORT_SKIPPED_SEGMENTS_PROMPT = (
|
||||
"Do you want to report skipped segments to sponsorblock. Only the segment"
|
||||
" UUID will be sent? (Y/n) "
|
||||
)
|
||||
MUTE_ADS_PROMPT = "Do you want to mute native YouTube ads automatically? (y/N) "
|
||||
SKIP_ADS_PROMPT = "Do you want to skip native YouTube ads automatically? (y/N) "
|
||||
AUTOPLAY_PROMPT = "Do you want to enable autoplay? (Y/n) "
|
||||
|
||||
|
||||
def get_yn_input(prompt):
|
||||
while choice := input(prompt):
|
||||
if choice.lower() in ["y", "n"]:
|
||||
return choice.lower()
|
||||
print("Invalid input. Please enter 'y' or 'n'.")
|
||||
|
||||
|
||||
async def create_web_session():
|
||||
return aiohttp.ClientSession()
|
||||
|
||||
|
||||
async def pair_device(web_session: aiohttp.ClientSession):
|
||||
try:
|
||||
lounge_controller = ytlounge.YtLoungeApi()
|
||||
await lounge_controller.change_web_session(web_session)
|
||||
pairing_code = input(PAIRING_CODE_PROMPT)
|
||||
pairing_code = int(
|
||||
pairing_code.replace("-", "").replace(" ", "")
|
||||
) # remove dashes and spaces
|
||||
print("Pairing...")
|
||||
paired = await lounge_controller.pair(pairing_code)
|
||||
if not paired:
|
||||
print("Failed to pair device")
|
||||
return
|
||||
device = {
|
||||
"screen_id": lounge_controller.auth.screen_id,
|
||||
"name": lounge_controller.screen_name,
|
||||
}
|
||||
print(f"Paired device: {device['name']}")
|
||||
return device
|
||||
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()
|
||||
web_session = loop.run_until_complete(create_web_session())
|
||||
if debug:
|
||||
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"
|
||||
)
|
||||
choice = get_yn_input(ATVS_REMOVAL_PROMPT)
|
||||
if choice == "y":
|
||||
del config["atvs"]
|
||||
|
||||
devices = config.devices
|
||||
choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices)))
|
||||
while choice == "y":
|
||||
device = loop.run_until_complete(pair_device(web_session))
|
||||
if device:
|
||||
devices.append(device)
|
||||
choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices)))
|
||||
config.devices = devices
|
||||
|
||||
apikey = config.apikey
|
||||
if apikey:
|
||||
choice = get_yn_input(CHANGE_API_KEY_PROMPT)
|
||||
if choice == "y":
|
||||
apikey = input(ENTER_API_KEY_PROMPT)
|
||||
else:
|
||||
choice = get_yn_input(ADD_API_KEY_PROMPT)
|
||||
if choice == "y":
|
||||
print(
|
||||
"Get youtube apikey here:"
|
||||
" https://developers.google.com/youtube/registering_an_application"
|
||||
)
|
||||
apikey = input(ENTER_API_KEY_PROMPT)
|
||||
config.apikey = apikey
|
||||
|
||||
skip_categories = config.skip_categories
|
||||
if skip_categories:
|
||||
choice = get_yn_input(CHANGE_SKIP_CATEGORIES_PROMPT)
|
||||
if choice == "y":
|
||||
categories = input(ENTER_SKIP_CATEGORIES_PROMPT)
|
||||
skip_categories = categories.replace(",", " ").split(" ")
|
||||
skip_categories = [
|
||||
x for x in skip_categories if x != ""
|
||||
] # Remove empty strings
|
||||
else:
|
||||
categories = input(ENTER_SKIP_CATEGORIES_PROMPT)
|
||||
skip_categories = categories.replace(",", " ").split(" ")
|
||||
skip_categories = [
|
||||
x for x in skip_categories if x != ""
|
||||
] # Remove empty strings
|
||||
config.skip_categories = skip_categories
|
||||
|
||||
channel_whitelist = config.channel_whitelist
|
||||
choice = get_yn_input(WHITELIST_CHANNELS_PROMPT)
|
||||
if choice == "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."
|
||||
)
|
||||
api_helper = api_helpers.ApiHelper(config, web_session)
|
||||
while True:
|
||||
channel_info = {}
|
||||
channel = input(SEARCH_CHANNEL_PROMPT)
|
||||
if channel == "/exit":
|
||||
break
|
||||
|
||||
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, item in enumerate(results):
|
||||
print(f"{i}: {item[1]} - Subs: {item[2]}")
|
||||
print("5: Enter a custom channel ID")
|
||||
print("6: Go back")
|
||||
|
||||
while choice := input(SELECT_CHANNEL_PROMPT):
|
||||
if choice in [str(x) for x in range(7)]:
|
||||
break
|
||||
print("Invalid choice")
|
||||
|
||||
if choice == "5":
|
||||
channel_info["id"] = input(ENTER_CHANNEL_ID_PROMPT)
|
||||
channel_info["name"] = input(ENTER_CUSTOM_CHANNEL_NAME_PROMPT)
|
||||
channel_whitelist.append(channel_info)
|
||||
continue
|
||||
if choice == "6":
|
||||
continue
|
||||
|
||||
channel_info["id"] = results[int(choice)][0]
|
||||
channel_info["name"] = results[int(choice)][1]
|
||||
channel_whitelist.append(channel_info)
|
||||
# Close web session asynchronously
|
||||
|
||||
config.channel_whitelist = channel_whitelist
|
||||
|
||||
choice = get_yn_input(REPORT_SKIPPED_SEGMENTS_PROMPT)
|
||||
config.skip_count_tracking = choice != "n"
|
||||
|
||||
choice = get_yn_input(MUTE_ADS_PROMPT)
|
||||
config.mute_ads = choice == "y"
|
||||
|
||||
choice = get_yn_input(SKIP_ADS_PROMPT)
|
||||
config.skip_ads = choice == "y"
|
||||
|
||||
choice = get_yn_input(AUTOPLAY_PROMPT)
|
||||
config.auto_play = choice != "n"
|
||||
|
||||
print("Config finished")
|
||||
config.save()
|
||||
loop.run_until_complete(web_session.close())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Send out an M-SEARCH request and listening for responses."""
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import rich_click as click
|
||||
from appdirs import user_data_dir
|
||||
|
||||
from . import config_setup, main, setup_wizard
|
||||
@@ -41,6 +41,8 @@ class Config:
|
||||
self.skip_count_tracking = True
|
||||
self.mute_ads = False
|
||||
self.skip_ads = False
|
||||
self.auto_play = True
|
||||
self.join_name = "iSponsorBlockTV"
|
||||
self.__load()
|
||||
|
||||
def validate(self):
|
||||
@@ -125,35 +127,93 @@ class Config:
|
||||
return False
|
||||
|
||||
|
||||
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=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")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = Config(args.data_dir)
|
||||
if args.debug:
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.option(
|
||||
"--data",
|
||||
"-d",
|
||||
default=lambda: os.getenv("iSPBTV_data_dir")
|
||||
or user_data_dir("iSponsorBlockTV", "dmunozv04"),
|
||||
help="data directory",
|
||||
)
|
||||
@click.option("--debug", is_flag=True, help="debug mode")
|
||||
# legacy commands as arguments
|
||||
@click.option(
|
||||
"--setup", is_flag=True, help="Setup the program graphically", hidden=True
|
||||
)
|
||||
@click.option(
|
||||
"--setup-cli",
|
||||
is_flag=True,
|
||||
help="Setup the program in the command line",
|
||||
hidden=True,
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(ctx, data, debug, setup, setup_cli):
|
||||
"""iSponsorblockTV"""
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["data_dir"] = data
|
||||
ctx.obj["debug"] = debug
|
||||
if debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
if args.setup: # Set up the config file graphically
|
||||
setup_wizard.main(config)
|
||||
sys.exit()
|
||||
if args.setup_cli: # Set up the config file
|
||||
config_setup.main(config, args.debug)
|
||||
else:
|
||||
config.validate()
|
||||
main.main(config, args.debug)
|
||||
if ctx.invoked_subcommand is None:
|
||||
if setup:
|
||||
ctx.invoke(setup_command)
|
||||
elif setup_cli:
|
||||
ctx.invoke(setup_cli_command)
|
||||
else:
|
||||
ctx.invoke(start)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def setup(ctx):
|
||||
"""Setup the program graphically"""
|
||||
config = Config(ctx.obj["data_dir"])
|
||||
setup_wizard.main(config)
|
||||
sys.exit()
|
||||
|
||||
|
||||
setup_command = setup
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def setup_cli(ctx):
|
||||
"""Setup the program in the command line"""
|
||||
config = Config(ctx.obj["data_dir"])
|
||||
config_setup.main(config, ctx.obj["debug"])
|
||||
|
||||
|
||||
setup_cli_command = setup_cli
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def start(ctx):
|
||||
"""Start the main program"""
|
||||
config = Config(ctx.obj["data_dir"])
|
||||
config.validate()
|
||||
main.main(config, ctx.obj["debug"])
|
||||
|
||||
|
||||
# Create fake "self" group to show pyapp options in help menu
|
||||
# Subcommands remove, restore, update
|
||||
pyapp_group = click.RichGroup("self", help="pyapp options (update, remove, restore)")
|
||||
pyapp_group.add_command(
|
||||
click.RichCommand("update", help="Update the package to the latest version")
|
||||
)
|
||||
pyapp_group.add_command(
|
||||
click.Command(
|
||||
"remove", help="Remove the package, wiping the installation but not the data"
|
||||
)
|
||||
)
|
||||
pyapp_group.add_command(
|
||||
click.RichCommand(
|
||||
"restore", help="Restore the package to its original state by reinstalling it"
|
||||
)
|
||||
)
|
||||
if os.getenv("PYAPP"):
|
||||
cli.add_command(pyapp_group)
|
||||
|
||||
|
||||
def app_start():
|
||||
cli(obj={})
|
||||
|
||||
@@ -27,9 +27,9 @@ class DeviceListener:
|
||||
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
)
|
||||
self.logger.addHandler(sh)
|
||||
self.logger.info(f"Starting device")
|
||||
self.logger.info("Starting device")
|
||||
self.lounge_controller = ytlounge.YtLoungeApi(
|
||||
device.screen_id, config, api_helper, self.logger, self.web_session
|
||||
device.screen_id, config, api_helper, self.logger
|
||||
)
|
||||
|
||||
# Ensures that we have a valid auth token
|
||||
@@ -38,14 +38,14 @@ class DeviceListener:
|
||||
await asyncio.sleep(60 * 60 * 24) # Refresh every 24 hours
|
||||
try:
|
||||
await self.lounge_controller.refresh_auth()
|
||||
except:
|
||||
except BaseException:
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
|
||||
async def is_available(self):
|
||||
try:
|
||||
return await self.lounge_controller.is_available()
|
||||
except:
|
||||
except BaseException:
|
||||
# traceback.print_exc()
|
||||
return False
|
||||
|
||||
@@ -57,20 +57,20 @@ class DeviceListener:
|
||||
try:
|
||||
self.logger.debug("Refreshing auth")
|
||||
await lounge_controller.refresh_auth()
|
||||
except:
|
||||
except BaseException:
|
||||
await asyncio.sleep(10)
|
||||
while not (await self.is_available()) and not self.cancelled:
|
||||
await asyncio.sleep(10)
|
||||
try:
|
||||
await lounge_controller.connect()
|
||||
except:
|
||||
except BaseException:
|
||||
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:
|
||||
except BaseException:
|
||||
pass
|
||||
self.logger.info(
|
||||
"Connected to device %s (%s)", lounge_controller.screen_name, self.name
|
||||
@@ -79,14 +79,14 @@ class DeviceListener:
|
||||
self.logger.info("Subscribing to lounge")
|
||||
sub = await lounge_controller.subscribe_monitored(self)
|
||||
await sub
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
# Method called on playback state change
|
||||
async def __call__(self, state):
|
||||
try:
|
||||
self.task.cancel()
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
time_start = time.time()
|
||||
self.task = asyncio.create_task(self.process_playstatus(state, time_start))
|
||||
@@ -131,40 +131,71 @@ class DeviceListener:
|
||||
await asyncio.create_task(self.api_helper.mark_viewed_segments(uuids))
|
||||
await asyncio.create_task(self.lounge_controller.seek_to(position))
|
||||
|
||||
# Stops the connection to the device
|
||||
async def cancel(self):
|
||||
self.cancelled = True
|
||||
try:
|
||||
await self.lounge_controller.disconnect()
|
||||
if self.task:
|
||||
self.task.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
if self.lounge_controller.subscribe_task_watchdog:
|
||||
self.lounge_controller.subscribe_task_watchdog.cancel()
|
||||
if self.lounge_controller.subscribe_task:
|
||||
self.lounge_controller.subscribe_task.cancel()
|
||||
await asyncio.gather(
|
||||
self.task,
|
||||
self.lounge_controller.subscribe_task_watchdog,
|
||||
self.lounge_controller.subscribe_task,
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
async def initialize_web_session(self):
|
||||
await self.lounge_controller.change_web_session(self.web_session)
|
||||
|
||||
|
||||
async def finish(devices):
|
||||
for i in devices:
|
||||
await i.cancel()
|
||||
async def finish(devices, web_session, tcp_connector):
|
||||
await asyncio.gather(
|
||||
*(device.cancel() for device in devices), return_exceptions=True
|
||||
)
|
||||
await web_session.close()
|
||||
await tcp_connector.close()
|
||||
|
||||
|
||||
def main(config, debug):
|
||||
def handle_signal(signum, frame):
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
|
||||
async def main_async(config, debug):
|
||||
loop = asyncio.get_event_loop_policy().get_event_loop()
|
||||
tasks = [] # Save the tasks so the interpreter doesn't garbage collect them
|
||||
devices = [] # Save the devices to close them later
|
||||
if debug:
|
||||
loop.set_debug(True)
|
||||
asyncio.set_event_loop(loop)
|
||||
tcp_connector = aiohttp.TCPConnector(ttl_dns_cache=300)
|
||||
web_session = aiohttp.ClientSession(loop=loop, connector=tcp_connector)
|
||||
web_session = aiohttp.ClientSession(connector=tcp_connector)
|
||||
api_helper = api_helpers.ApiHelper(config, web_session)
|
||||
for i in config.devices:
|
||||
device = DeviceListener(api_helper, config, i, debug, web_session)
|
||||
devices.append(device)
|
||||
await device.initialize_web_session()
|
||||
tasks.append(loop.create_task(device.loop()))
|
||||
tasks.append(loop.create_task(device.refresh_auth_loop()))
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
signal(SIGTERM, lambda s, f: loop.stop())
|
||||
loop.run_forever()
|
||||
print("Cancelling tasks and exiting...")
|
||||
loop.run_until_complete(finish(devices))
|
||||
loop.run_until_complete(web_session.close())
|
||||
loop.run_until_complete(tcp_connector.close())
|
||||
signal(SIGTERM, handle_signal)
|
||||
signal(SIGINT, handle_signal)
|
||||
try:
|
||||
await asyncio.gather(*tasks)
|
||||
except KeyboardInterrupt:
|
||||
print("Cancelling tasks and exiting...")
|
||||
await finish(devices, web_session, tcp_connector)
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
finally:
|
||||
await web_session.close()
|
||||
await tcp_connector.close()
|
||||
loop.close()
|
||||
print("Exited")
|
||||
|
||||
|
||||
def main(config, debug):
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main_async(config, debug))
|
||||
loop.close()
|
||||
|
||||
@@ -21,9 +21,13 @@
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.small-button{
|
||||
.button-small {
|
||||
height: 3;
|
||||
|
||||
border: none;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
offset: 0 -1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button-100 {
|
||||
@@ -106,13 +110,14 @@ EditDevice {
|
||||
}
|
||||
|
||||
Element {
|
||||
background: $panel;
|
||||
background: $panel-darken-1;
|
||||
border-top: solid $panel-lighten-2;
|
||||
layout: horizontal;
|
||||
height: 2;
|
||||
width: 100%;
|
||||
margin: 0 1 0 1;
|
||||
padding: 0;
|
||||
|
||||
}
|
||||
Element > .element-name {
|
||||
offset: 0 -1;
|
||||
@@ -120,7 +125,11 @@ Element > .element-name {
|
||||
width: 100%;
|
||||
align: left middle;
|
||||
text-align: left;
|
||||
|
||||
background: $panel-darken-1;
|
||||
&:hover {
|
||||
background: $panel-lighten-1;
|
||||
border-top: tall $panel-lighten-3;
|
||||
}
|
||||
}
|
||||
Element > .element-remove {
|
||||
dock: right;
|
||||
@@ -132,7 +141,7 @@ Element > .element-remove {
|
||||
margin: 0 1 0 0;
|
||||
}
|
||||
|
||||
#add-device {
|
||||
#add-device, #add-channel {
|
||||
text-style: bold;
|
||||
width: 100%;
|
||||
align: left middle;
|
||||
@@ -140,6 +149,11 @@ Element > .element-remove {
|
||||
dock: left;
|
||||
|
||||
text-align: left;
|
||||
background: $panel-darken-1;
|
||||
&:hover {
|
||||
background: $panel-lighten-1;
|
||||
border-top: tall $panel-lighten-3;
|
||||
}
|
||||
}
|
||||
#add-device-button-container{
|
||||
height: 1;
|
||||
@@ -363,3 +377,9 @@ MigrationScreen {
|
||||
width: 1fr;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
/* Autoplay */
|
||||
#autoplay-container{
|
||||
padding: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -84,12 +84,15 @@ class Element(Static):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Button(
|
||||
label=self.element_name,
|
||||
classes="element-name",
|
||||
classes="element-name button-small",
|
||||
disabled=True,
|
||||
id="element-name",
|
||||
)
|
||||
yield Button(
|
||||
"Remove", classes="element-remove", variant="error", id="element-remove"
|
||||
"Remove",
|
||||
classes="element-remove button-small",
|
||||
variant="error",
|
||||
id="element-remove",
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
@@ -102,7 +105,6 @@ class Device(Element):
|
||||
"""A device element."""
|
||||
|
||||
def process_values_from_data(self):
|
||||
print(self.element_data)
|
||||
if "name" in self.element_data and self.element_data["name"]:
|
||||
self.element_name = self.element_data["name"]
|
||||
else:
|
||||
@@ -285,7 +287,8 @@ class AddDevice(ModalWithClickExit):
|
||||
" computer\nIf it isn't showing up, try restarting the"
|
||||
" app.\nIf running in docker, make sure to use"
|
||||
" `--network=host`\nTo refresh the list, close and open the"
|
||||
" dialog again"
|
||||
" dialog again\n[b][u]If it still doesn't work, "
|
||||
"pair using a pairing code (it's much more reliable)"
|
||||
),
|
||||
classes="subtitle",
|
||||
)
|
||||
@@ -328,17 +331,18 @@ class AddDevice(ModalWithClickExit):
|
||||
|
||||
@on(Input.Changed, "#pairing-code-input")
|
||||
def changed_pairing_code(self, event: Input.Changed):
|
||||
self.query_one("#add-device-pin-add-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-pin-add-button").disabled = True
|
||||
lounge_controller = ytlounge.YtLoungeApi(
|
||||
"iSponsorBlockTV", web_session=self.web_session
|
||||
"iSponsorBlockTV",
|
||||
)
|
||||
await lounge_controller.change_web_session(self.web_session)
|
||||
pairing_code = self.query_one("#pairing-code-input").value
|
||||
pairing_code = int(
|
||||
pairing_code.replace("-", "").replace(" ", "")
|
||||
@@ -347,7 +351,7 @@ class AddDevice(ModalWithClickExit):
|
||||
paired = False
|
||||
try:
|
||||
paired = await lounge_controller.pair(pairing_code)
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
if paired:
|
||||
device = {
|
||||
@@ -377,9 +381,9 @@ class AddDevice(ModalWithClickExit):
|
||||
|
||||
@on(SelectionList.SelectedChanged, "#dial-devices-list")
|
||||
def changed_device_list(self, event: SelectionList.SelectedChanged):
|
||||
self.query_one("#add-device-dial-add-button").disabled = (
|
||||
not event.selection_list.selected
|
||||
)
|
||||
self.query_one(
|
||||
"#add-device-dial-add-button"
|
||||
).disabled = not event.selection_list.selected
|
||||
|
||||
|
||||
class AddChannel(ModalWithClickExit):
|
||||
@@ -476,7 +480,6 @@ class AddChannel(ModalWithClickExit):
|
||||
|
||||
@on(Button.Pressed, "#add-channel-switch-buttons > *")
|
||||
def handle_switch_buttons(self, event: Button.Pressed) -> None:
|
||||
button_ = event.button.id
|
||||
self.query_one("#add-channel-switcher").current = event.button.id.replace(
|
||||
"-button", "-container"
|
||||
)
|
||||
@@ -496,7 +499,7 @@ class AddChannel(ModalWithClickExit):
|
||||
self.query_one("#channel-search-results").remove_children()
|
||||
try:
|
||||
channels_list = await self.api_helper.search_channels(channel_name)
|
||||
except:
|
||||
except BaseException:
|
||||
self.query_one("#add-channel-info").update(
|
||||
"[#ff0000]Failed to search for channel"
|
||||
)
|
||||
@@ -585,7 +588,7 @@ class EditDevice(ModalWithClickExit):
|
||||
)
|
||||
|
||||
def on_slider_changed(self, event: Slider.Changed) -> None:
|
||||
offset_input = self.query_one("#device-offset-offset_input")
|
||||
offset_input = self.query_one("#device-offset-input")
|
||||
with offset_input.prevent(Input.Changed):
|
||||
offset_input.value = str(event.slider.value)
|
||||
|
||||
@@ -619,7 +622,9 @@ class DevicesManager(Vertical):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("Devices", classes="title")
|
||||
with Horizontal(id="add-device-button-container"):
|
||||
yield Button("Add Device", id="add-device", classes="button-100")
|
||||
yield Button(
|
||||
"Add Device", id="add-device", classes="button-100 button-small"
|
||||
)
|
||||
for device in self.devices:
|
||||
yield Device(device, tooltip="Click to edit")
|
||||
|
||||
@@ -813,7 +818,9 @@ class ChannelWhitelistManager(Vertical):
|
||||
id="warning-no-key",
|
||||
)
|
||||
with Horizontal(id="add-channel-button-container"):
|
||||
yield Button("Add Channel", id="add-channel", classes="button-100")
|
||||
yield Button(
|
||||
"Add Channel", id="add-channel", classes="button-100 button-small"
|
||||
)
|
||||
for channel in self.config.channel_whitelist:
|
||||
yield Channel(channel)
|
||||
|
||||
@@ -850,6 +857,32 @@ class ChannelWhitelistManager(Vertical):
|
||||
self.app.push_screen(AddChannel(self.config), callback=self.new_channel)
|
||||
|
||||
|
||||
class AutoPlayManager(Vertical):
|
||||
"""Manager for autoplay, allows enabling/disabling autoplay."""
|
||||
|
||||
def __init__(self, config, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.config = config
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("Autoplay", classes="title")
|
||||
yield Label(
|
||||
"This feature allows you to enable/disable autoplay",
|
||||
classes="subtitle",
|
||||
id="autoplay-subtitle",
|
||||
)
|
||||
with Horizontal(id="autoplay-container"):
|
||||
yield Checkbox(
|
||||
value=self.config.auto_play,
|
||||
id="autoplay-switch",
|
||||
label="Enable autoplay",
|
||||
)
|
||||
|
||||
@on(Checkbox.Changed, "#autoplay-switch")
|
||||
def changed_skip(self, event: Checkbox.Changed):
|
||||
self.config.auto_play = event.checkbox.value
|
||||
|
||||
|
||||
class ISponsorBlockTVSetupMainScreen(Screen):
|
||||
"""Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221"""
|
||||
|
||||
@@ -886,6 +919,9 @@ class ISponsorBlockTVSetupMainScreen(Screen):
|
||||
yield ApiKeyManager(
|
||||
config=self.config, id="api-key-manager", classes="container"
|
||||
)
|
||||
yield AutoPlayManager(
|
||||
config=self.config, id="autoplay-manager", classes="container"
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
if self.check_for_old_config_entries():
|
||||
@@ -914,7 +950,7 @@ class ISponsorBlockTVSetupMainScreen(Screen):
|
||||
self.app.query_one("#warning-no-key").display = (
|
||||
not event.input.value
|
||||
) and self.config.channel_whitelist
|
||||
except:
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, List
|
||||
|
||||
import pyytlounge
|
||||
from aiohttp import ClientSession
|
||||
@@ -12,15 +13,14 @@ create_task = asyncio.create_task
|
||||
class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
def __init__(
|
||||
self,
|
||||
screen_id,
|
||||
screen_id=None,
|
||||
config=None,
|
||||
api_helper=None,
|
||||
logger=None,
|
||||
web_session: ClientSession = None,
|
||||
):
|
||||
super().__init__("iSponsorBlockTV", logger=logger)
|
||||
if web_session is not None:
|
||||
self.session = web_session # And use the one we passed
|
||||
super().__init__(
|
||||
config.join_name if config else "iSponsorBlockTV", logger=logger
|
||||
)
|
||||
self.auth.screen_id = screen_id
|
||||
self.auth.lounge_id_token = None
|
||||
self.api_helper = api_helper
|
||||
@@ -30,9 +30,12 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
self.callback = None
|
||||
self.logger = logger
|
||||
self.shorts_disconnected = False
|
||||
self.auto_play = True
|
||||
if config:
|
||||
self.mute_ads = config.mute_ads
|
||||
self.skip_ads = config.skip_ads
|
||||
self.auto_play = config.auto_play
|
||||
self._command_mutex = asyncio.Lock()
|
||||
|
||||
# Ensures that we still are subscribed to the lounge
|
||||
async def _watchdog(self):
|
||||
@@ -41,7 +44,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
) # YouTube sends at least a message every 30 seconds (no-op or any other)
|
||||
try:
|
||||
self.subscribe_task.cancel()
|
||||
except Exception:
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
# Subscribe to the lounge and start the watchdog
|
||||
@@ -49,25 +52,27 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
self.callback = callback
|
||||
try:
|
||||
self.subscribe_task_watchdog.cancel()
|
||||
except:
|
||||
except BaseException:
|
||||
pass # No watchdog task
|
||||
self.subscribe_task = asyncio.create_task(super().subscribe(callback))
|
||||
self.subscribe_task_watchdog = asyncio.create_task(self._watchdog())
|
||||
create_task(self.debug_command("bugchomp "))
|
||||
return self.subscribe_task
|
||||
|
||||
# Process a lounge subscription event
|
||||
def _process_event(self, event_id: int, event_type: str, args):
|
||||
self.logger.debug(f"process_event({event_id}, {event_type}, {args})")
|
||||
def _process_event(self, event_type: str, args: List[Any]):
|
||||
self.logger.debug(f"process_event({event_type}, {args})")
|
||||
# (Re)start the watchdog
|
||||
try:
|
||||
self.subscribe_task_watchdog.cancel()
|
||||
except:
|
||||
except BaseException:
|
||||
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)
|
||||
if event_type == "onStateChange":
|
||||
create_task(self.debug_command("exp 0 "))
|
||||
data = args[0]
|
||||
# print(data)
|
||||
# Unmute when the video starts playing
|
||||
@@ -143,11 +148,16 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
self.shorts_disconnected = False
|
||||
create_task(self.play_video(video_id_saved))
|
||||
elif event_type == "loungeScreenDisconnected":
|
||||
data = args[0]
|
||||
if data["reason"] == "disconnectedByUserScreenInitiated": # Short playing?
|
||||
self.shorts_disconnected = True
|
||||
if args: # Sometimes it's empty
|
||||
data = args[0]
|
||||
if (
|
||||
data["reason"] == "disconnectedByUserScreenInitiated"
|
||||
): # Short playing?
|
||||
self.shorts_disconnected = True
|
||||
elif event_type == "onAutoplayModeChanged":
|
||||
create_task(self.set_auto_play_mode(self.auto_play))
|
||||
|
||||
super()._process_event(event_id, event_type, args)
|
||||
super()._process_event(event_type, args)
|
||||
|
||||
# Set the volume to a specific value (0-100)
|
||||
async def set_volume(self, volume: int) -> None:
|
||||
@@ -177,3 +187,25 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
|
||||
|
||||
async def play_video(self, video_id: str) -> bool:
|
||||
return await self._command("setPlaylist", {"videoId": video_id})
|
||||
|
||||
# Test to wrap the command function in a mutex to avoid race conditions with
|
||||
# the _command_offset (TODO: move to upstream if it works)
|
||||
async def _command(self, command: str, command_parameters: dict = None) -> bool:
|
||||
async with self._command_mutex:
|
||||
self.logger.debug(
|
||||
f"Send command: {command}, Parameters: {command_parameters}"
|
||||
)
|
||||
return await super()._command(command, command_parameters)
|
||||
|
||||
async def change_web_session(self, web_session: ClientSession):
|
||||
if self.session is not None:
|
||||
await self.session.close()
|
||||
if self.conn is not None:
|
||||
await self.conn.close()
|
||||
self.session = web_session
|
||||
|
||||
async def debug_command(self, debug_command: str):
|
||||
await super()._command(
|
||||
"sendDebugCommand",
|
||||
{"debugCommand": debug_command},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user