Compare commits

...

451 Commits

Author SHA1 Message Date
dependabot[bot]
b985309944 Bump aiohttp from 3.12.15 to 3.13.1
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.13.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 20:08:47 +00:00
dmunozv04
12bed77cca bump version 2025-10-19 19:42:00 +02:00
David
dab84dec96 Merge pull request #374 from dmunozv04/dependabot/github_actions/peter-evans/dockerhub-description-5
Bump peter-evans/dockerhub-description from 4 to 5
2025-10-13 09:26:40 +02:00
dependabot[bot]
aaf1f64ec7 Bump peter-evans/dockerhub-description from 4 to 5
Bumps [peter-evans/dockerhub-description](https://github.com/peter-evans/dockerhub-description) from 4 to 5.
- [Release notes](https://github.com/peter-evans/dockerhub-description/releases)
- [Commits](https://github.com/peter-evans/dockerhub-description/compare/v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/dockerhub-description
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 20:15:45 +00:00
David
49ea01dd9c Merge pull request #362 from dmunozv04:clarify-muting-not-working-airplay
Clarify ad muting not working when using airplay
2025-09-24 14:40:39 +02:00
dmunozv04
2a2949f552 Clarify ad muting not working when using airplay 2025-09-24 14:38:42 +02:00
David
85b4124a52 Merge pull request #359 from Wobak/patch-1
Update docker-compose.yml
2025-09-23 11:48:03 +02:00
Wobak
da7dcf67fe Update docker-compose.yml
removed version as not recommended anymore and changed indentation from tabs to spaces
2025-09-22 20:10:17 +02:00
David
b4d1feb3a9 Merge pull request #358 from dmunozv04/semver-docker-tags
Add semver tags to docker build
2025-09-21 11:23:20 +02:00
dmunozv04
6afd1bcbaa Add semver tags to docker build 2025-09-18 10:40:52 +02:00
David
516326e0ff Merge pull request #351 from dmunozv04/dependabot/pip/ssdp-1.3.1
Bump ssdp from 1.3.0 to 1.3.1
2025-09-11 13:44:31 +02:00
dependabot[bot]
461b8bfde7 Bump ssdp from 1.3.0 to 1.3.1
Bumps [ssdp](https://github.com/codingjoe/ssdp) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/codingjoe/ssdp/releases)
- [Commits](https://github.com/codingjoe/ssdp/compare/1.3.0...1.3.1)

---
updated-dependencies:
- dependency-name: ssdp
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 11:44:03 +00:00
David
580ac5e3e1 Merge pull request #350 from dmunozv04/dependabot/pip/xmltodict-0.15.1
Bump xmltodict from 0.14.2 to 0.15.1
2025-09-11 13:42:55 +02:00
dependabot[bot]
52a221c4e0 Bump xmltodict from 0.14.2 to 0.15.1
Bumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.14.2 to 0.15.1.
- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)
- [Commits](https://github.com/martinblech/xmltodict/compare/v0.14.2...v0.15.1)

---
updated-dependencies:
- dependency-name: xmltodict
  dependency-version: 0.15.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 13:42:44 +02:00
David
e6dff63b19 Merge pull request #340 from dmunozv04/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-09-11 13:41:39 +02:00
pre-commit-ci[bot]
8bab77237d [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.12.7 → v0.12.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.7...v0.12.12)
2025-09-11 13:41:30 +02:00
David
31a6d260e5 Merge pull request #343 from dmunozv04/dependabot/github_actions/actions/download-artifact-5
Bump actions/download-artifact from 4 to 5
2025-09-11 13:40:49 +02:00
dependabot[bot]
34256b5c5e Bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 13:40:41 +02:00
David
91842c18f2 Merge pull request #353 from dmunozv04/dependabot/github_actions/actions/attest-build-provenance-3
Bump actions/attest-build-provenance from 2 to 3
2025-09-11 13:40:21 +02:00
dependabot[bot]
accb685bf3 Bump actions/attest-build-provenance from 2 to 3
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2 to 3.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 13:40:09 +02:00
David
f68311cbf6 Merge pull request #354 from dmunozv04/dependabot/github_actions/actions/setup-python-6
Bump actions/setup-python from 5 to 6
2025-09-11 13:39:43 +02:00
dependabot[bot]
aa76d130d8 Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 20:30:21 +00:00
David
258239338e Merge pull request #342 from dmunozv04/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-31 12:26:37 -04:00
dependabot[bot]
21f52537d8 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-31 16:22:01 +00:00
David
f6bfd9af98 Merge pull request #341 from dmunozv04/dependabot/pip/aiohttp-3.12.15
Bump aiohttp from 3.12.14 to 3.12.15
2025-08-31 12:19:59 -04:00
dependabot[bot]
4a57cce9bb Bump aiohttp from 3.12.14 to 3.12.15
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.12.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-30 17:59:01 +00:00
David
6524361d5d Merge pull request #348 from dmunozv04/update-ci-2
Only login to dockerhub if username is provided via secret
2025-08-30 13:57:47 -04:00
dmunozv04
27ecc54d93 Only login to dockerhub if username is provided via secret 2025-08-30 13:57:38 -04:00
David
3b7617ef14 Merge pull request #345 from gaiar/add-legacy-architecture-support
Add support for legacy architectures (i386 and armv6)
2025-08-30 13:48:44 -04:00
Gaiar Baimuratov
aa35610c67 Add support for legacy architectures (i386 and armv6)
Extends Docker build to include:
- linux/386: 32-bit x86 systems (legacy servers, older PCs)
- linux/arm/v6: ARMv6 devices (Raspberry Pi Zero, Pi 1, older ARM systems)

Motivation: Many users have legacy but capable hardware that can effectively
run containerized applications. These older systems often have sufficient
resources for iSponsorBlockTV but lack support in most modern Docker images.

Technical impact:
- Zero breaking changes for existing users
- Leverages existing QEMU cross-compilation infrastructure
- Minimal build time increase (~10-15%)
- Broader device compatibility without maintenance overhead
2025-08-30 13:48:06 -04:00
David
d581f7ee07 Merge pull request #347 from dmunozv04/update-ci
Update ci
2025-08-30 13:47:50 -04:00
dmunozv04
8e01755550 Only build on main/release/tag 2025-08-30 19:39:28 +02:00
dmunozv04
56b42e26ff Update release
- update pyapp
- ignore attestation errors
2025-08-21 13:57:24 +02:00
dmunozv04
c88861822c Fix markdownlint 2025-08-10 19:33:41 +02:00
dmunozv04
315ac2c726 Update README.md 2025-08-10 19:28:12 +02:00
dmunozv04
390eb68310 Bump version 2025-08-10 19:05:59 +02:00
pre-commit-ci[bot]
7652c9f260 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.9 → v0.12.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.9...v0.12.7)
- [github.com/igorshubovych/markdownlint-cli: v0.44.0 → v0.45.0](https://github.com/igorshubovych/markdownlint-cli/compare/v0.44.0...v0.45.0)
2025-08-10 18:51:03 +02:00
dmunozv04
acf074e860 Various fixes to work with newer textual versions 2025-08-10 18:50:32 +02:00
dependabot[bot]
123f3d4000 Bump textual from 2.1.2 to 5.0.1
Bumps [textual](https://github.com/Textualize/textual) from 2.1.2 to 5.0.1.
- [Release notes](https://github.com/Textualize/textual/releases)
- [Changelog](https://github.com/Textualize/textual/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Textualize/textual/compare/v2.1.2...v5.0.1)

---
updated-dependencies:
- dependency-name: textual
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-10 18:50:32 +02:00
dependabot[bot]
303d805e5d Bump rich from 14.0.0 to 14.1.0
Bumps [rich](https://github.com/Textualize/rich) from 14.0.0 to 14.1.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.0.0...v14.1.0)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 14.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-10 17:54:50 +02:00
dependabot[bot]
c779e83d96 Bump rich-click from 1.8.8 to 1.8.9
Bumps [rich-click](https://github.com/ewels/rich-click) from 1.8.8 to 1.8.9.
- [Release notes](https://github.com/ewels/rich-click/releases)
- [Changelog](https://github.com/ewels/rich-click/blob/v1.8.9/CHANGELOG.md)
- [Commits](https://github.com/ewels/rich-click/compare/v1.8.8...v1.8.9)

---
updated-dependencies:
- dependency-name: rich-click
  dependency-version: 1.8.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-10 17:52:30 +02:00
dependabot[bot]
aac4b333c2 Bump aiohttp from 3.11.18 to 3.12.14
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.11.18 to 3.12.14.
- [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.11.18...v3.12.14)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.12.14
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-10 17:51:24 +02:00
David
fa83124002 Merge pull request #334 from desofity/trust-system-proxy-by-default
Added proxy support
2025-07-28 22:19:04 +02:00
pre-commit-ci[bot]
76b82e8848 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-07-28 17:23:53 +00:00
desofity
26fec272a3 fixed prompts 2025-07-28 20:22:50 +03:00
desofity
3fdcee71fd Merge branch 'trust-system-proxy-by-default' of https://github.com/desofity/iSponsorBlockTV into trust-system-proxy-by-default 2025-07-18 14:25:58 +03:00
desofity
1a58ce6a57 split label text to pass deepSource python check 2025-07-18 14:23:08 +03:00
pre-commit-ci[bot]
90d313049b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-07-18 11:19:13 +00:00
desofity
cd7e5c83c7 added param to Config constructor 2025-07-18 14:12:15 +03:00
desofity
bf1fe68089 messing with gui 3 2025-07-18 00:06:21 +03:00
desofity
d179fe2b79 messing with gui 2 2025-07-17 23:52:25 +03:00
desofity
4724ee1a39 messing with gui 2025-07-17 23:40:25 +03:00
desofity
bb7fdbfb06 option added to GUI setup 2025-07-17 23:19:47 +03:00
desofity
930db16f53 added prompt for use_proxy in config_setup 2025-07-17 22:05:03 +03:00
desofity
8ed1cb4b00 added use_proxy setting in configuration file 2025-07-17 22:00:57 +03:00
desofity
65ecbb9193 trying to just fix aiohttp.ClientSession call 2025-07-17 21:17:46 +03:00
dmunozv04
f2155abad3 Bump version 2025-05-30 21:56:45 +02:00
David
edbea793ed Merge pull request #312 from dmunozv04/fix-311
Fixes constant "new decive connected"
2025-05-30 21:55:46 +02:00
dmunozv04
df629805c2 Fixes constant "new decive connected" 2025-05-30 21:55:00 +02:00
dmunozv04
ad9834b9f0 Bump version 2025-05-30 09:56:24 +02:00
David
97e7b31d9c Merge pull request #310 from dmunozv04/fix-error-401-connect
Fix error 401 connect
2025-05-28 23:52:45 +02:00
David
b5d275e01e Merge branch 'main' into fix-error-401-connect 2025-05-28 23:48:35 +02:00
pre-commit-ci[bot]
98c1211b09 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-05-28 21:48:24 +00:00
David
57f33ec354 Merge pull request #309 from dmunozv04/http-tracing
Add http tracing
2025-05-28 23:47:04 +02:00
pre-commit-ci[bot]
9f6a18a006 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-05-28 21:43:15 +00:00
dmunozv04
fd6f0d7283 Attempt to fix the issue 2025-05-28 00:18:32 +02:00
dmunozv04
166e238f41 Mimick YouTube iOS app 2025-05-25 14:02:59 +02:00
dmunozv04
8ecaa7e86f Add http tracing 2025-05-22 00:33:36 +02:00
dmunozv04
cafdf4f962 Bump version 2025-05-19 10:24:05 +02:00
David
3a80c76fb6 Merge pull request #292 from dmunozv04/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-05-19 10:16:30 +02:00
pre-commit-ci[bot]
a4f6462026 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.4 → v0.11.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.4...v0.11.9)
2025-05-19 10:16:12 +02:00
David
c4571ad90b Merge pull request #296 from dmunozv04/dependabot/pip/aiohttp-3.11.18
Bump aiohttp from 3.11.16 to 3.11.18
2025-05-19 10:15:36 +02:00
dependabot[bot]
c51c47b566 Bump aiohttp from 3.11.16 to 3.11.18
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.11.16 to 3.11.18.
- [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.11.16...v3.11.18)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.11.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 08:15:16 +00:00
David
72ada4558e Merge pull request #300 from dmunozv04/improve-watchdog
Improve watchdog/ Fix ad muting/skipping
2025-05-14 23:56:03 +02:00
David
6219cfe0d0 Merge branch 'main' into improve-watchdog 2025-05-14 23:54:57 +02:00
pre-commit-ci[bot]
b2dfe35698 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-05-14 21:54:36 +00:00
David
af35982ac0 Merge pull request #278 from sternma/minimum-skip-length
Add support for minimum skip length
2025-05-14 23:52:17 +02:00
Matthew Stern
fb30d7e4cb Merge branch 'dmunozv04:main' into minimum-skip-length 2025-05-14 00:16:42 -04:00
pre-commit-ci[bot]
f04f7560b2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-05-14 04:16:24 +00:00
Matthew Stern
d55fceda18 Merge pull request #1 from SimoMay/minimum-skip-length
Fix feedback for #278 (duplicate code)
2025-05-14 00:16:16 -04:00
dmunozv04
a6dacc1d84 Fix adPlaying logic 2025-05-11 01:37:28 +02:00
dmunozv04
a67e3eb860 Compare strings 2025-05-10 22:28:01 +02:00
dmunozv04
a9d64af2ac Fix muting not working 2025-05-10 18:27:18 +02:00
dmunozv04
fc8d1770cd Improve watchdog 2025-05-03 21:42:13 +02:00
Mohamed
82ce3e60e9 Remove redundant code for minimum skip length configuration 2025-04-29 17:52:11 +02:00
Mohamed
4417592b6b Fix: Correct line length error in setup wizard label
Split long string literal in the Minimum Skip Length label to adhere
to maximum line length constraints (FLK-E501)
2025-04-29 17:39:52 +02:00
David
7187ec5286 Merge pull request #293 from dmunozv04:use-mutex-all-commands
Use command mutex for all commands
2025-04-15 13:58:59 +02:00
dmunozv04
6ea500222e Use command mutex for all commands 2025-04-15 13:57:35 +02:00
David
905a74c103 Use arm64 runners for binary creation (#236)
* test the new arm64 runners
https://github.blog/changelog/2025-01-16-linux-arm64-hosted-runners-now-available-for-free-in-public-repositories-public-preview/

* Enable build provenance attestation

* Add permissions

* enable cross on arm64 linux builds

* update pyapp to v0.27
2025-04-12 20:14:04 +02:00
David
35bc1ea6dc Merge pull request #288 from dmunozv04/create_event_loop
Use asyncio.run to create new event loop
2025-04-12 20:02:47 +02:00
dmunozv04
c3f28f7cd1 Use asyncio.run to create new event loop 2025-04-12 19:59:54 +02:00
David
724b88a2ba Merge pull request #289 from dmunozv04/dependabot/pip/aiohttp-3.11.16
Bump aiohttp from 3.11.13 to 3.11.16
2025-04-12 19:57:15 +02:00
dependabot[bot]
18105e4aa8 Bump aiohttp from 3.11.13 to 3.11.16
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.11.13 to 3.11.16.
- [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.11.13...v3.11.16)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.11.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-12 17:56:59 +00:00
David
8f60a2650a Merge pull request #286 from dmunozv04/dependabot/pip/rich-14.0.0
Bump rich from 13.9.4 to 14.0.0
2025-04-12 19:55:50 +02:00
dependabot[bot]
5888b8f9c6 Bump rich from 13.9.4 to 14.0.0
Bumps [rich](https://github.com/Textualize/rich) from 13.9.4 to 14.0.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v13.9.4...v14.0.0)

---
updated-dependencies:
- dependency-name: rich
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-12 17:55:06 +00:00
David
f890434dbf Merge pull request #279 from dmunozv04/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-04-12 19:54:37 +02:00
pre-commit-ci[bot]
6a32e42671 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.10 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.10...v0.11.4)
2025-04-12 19:54:03 +02:00
dependabot[bot]
ad35ecc778 Bump textual from 1.0.0 to 2.1.2 (#265)
* Bump textual from 1.0.0 to 2.1.2

Bumps [textual](https://github.com/Textualize/textual) from 1.0.0 to 2.1.2.
- [Release notes](https://github.com/Textualize/textual/releases)
- [Changelog](https://github.com/Textualize/textual/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Textualize/textual/compare/v1.0.0...v2.1.2)

---
updated-dependencies:
- dependency-name: textual
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Quote markdown link

Compatibility with textual >= 2.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dmunozv04 <39565245+dmunozv04@users.noreply.github.com>
2025-04-12 19:53:11 +02:00
David
e9925b02c3 Merge pull request #290 from dmunozv04/line-length-100
Change max line length to 100
2025-04-12 19:51:30 +02:00
dmunozv04
ffdeb4579e Ruff formatting 2025-04-12 19:49:25 +02:00
dmunozv04
70ecf78f01 Set line length to 100 2025-04-12 19:49:15 +02:00
David
1e10ab4e29 Merge pull request #277 from sternma/feat-use-upstream-autoplay
remove set_auto_play_mode
2025-04-12 15:09:50 +02:00
Matthew Stern
dee71939c5 Merge branch 'dmunozv04:main' into feat-use-upstream-autoplay 2025-03-27 09:55:15 -04:00
Matthew Stern
2dbeed99bc Merge branch 'dmunozv04:main' into minimum-skip-length 2025-03-27 09:53:06 -04:00
dmunozv04
451ffce47b Bump version to 2.4.0 2025-03-24 10:21:16 +01:00
pre-commit-ci[bot]
2124fff81b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-14 23:30:01 +00:00
Matthew Stern
aabf5aa2bc Merge remote-tracking branch 'origin/minimum-skip-length' into minimum-skip-length 2025-03-14 19:29:49 -04:00
Matthew Stern
068623bb03 Add input validation for min skip length 2025-03-14 19:28:54 -04:00
pre-commit-ci[bot]
b93f480848 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-14 23:11:31 +00:00
Matthew Stern
e9fdc49480 Add minimum skip length input 2025-03-14 19:06:15 -04:00
Matthew Stern
4a55fe9539 set minimum skip length to 1 2025-03-14 19:05:55 -04:00
Matthew Stern
328e70a175 add minimum skip length manager 2025-03-14 19:00:05 -04:00
Matthew Stern
7a1d8967ae set default minimum skip length to 1 2025-03-14 18:42:47 -04:00
pre-commit-ci[bot]
33b0b6d224 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-14 18:39:52 -04:00
bourkemcrobbo
e0c4322524 Updated setup to match new format. Set default value of skip length to 0 so user has to explicitly enable functionality 2025-03-14 18:39:52 -04:00
pre-commit-ci[bot]
c360e2582e [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-14 18:39:16 -04:00
bourkemcrobbo
7b3e618628 Fixed bad static method argument 2025-03-14 18:39:16 -04:00
pre-commit-ci[bot]
886997beab [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-14 18:39:16 -04:00
bourkemcrobbo
ee786a53b9 Fixed bad static method argument 2025-03-14 18:35:54 -04:00
bourkemcrobbo
0b785da448 Added support for specifying minimum skip length 2025-03-14 18:35:54 -04:00
Matthew Stern
ca9b7ee73a remove set_auto_play_mode 2025-03-13 23:19:44 -04:00
David
c21ebe396e Merge pull request #275 from dmunozv04/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-03-11 18:50:04 +01:00
pre-commit-ci[bot]
a5af3dfb1c [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.9.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.9.10)
2025-03-11 18:49:40 +01:00
David
7e3318dceb Merge pull request #272 from dmunozv04/dependabot/pip/rich-click-1.8.8
Bump rich-click from 1.8.6 to 1.8.8
2025-03-11 18:48:47 +01:00
dependabot[bot]
db7f0511a4 Bump rich-click from 1.8.6 to 1.8.8
Bumps [rich-click](https://github.com/ewels/rich-click) from 1.8.6 to 1.8.8.
- [Release notes](https://github.com/ewels/rich-click/releases)
- [Changelog](https://github.com/ewels/rich-click/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ewels/rich-click/compare/v1.8.6...v1.8.8)

---
updated-dependencies:
- dependency-name: rich-click
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-11 18:48:04 +01:00
David
6d7c7c00a4 Merge pull request #273 from dmunozv04/dependabot/pip/pyytlounge-2.3.0
Bump pyytlounge from 2.2.1 to 2.3.0
2025-03-10 20:38:18 +01:00
dependabot[bot]
b81a023b0d Bump pyytlounge from 2.2.1 to 2.3.0
Bumps [pyytlounge](https://github.com/FabioGNR/pyytlounge) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/FabioGNR/pyytlounge/releases)
- [Changelog](https://github.com/FabioGNR/pyytlounge/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FabioGNR/pyytlounge/compare/v2.2.1...v2.3.0)

---
updated-dependencies:
- dependency-name: pyytlounge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 19:37:31 +00:00
David
33d8fb419f Merge pull request #271 from dmunozv04/clean-code
Clean code
2025-03-10 13:07:36 +01:00
David
2630228b7b Merge branch 'main' into clean-code 2025-03-10 13:06:03 +01:00
David
712e8f37f2 Merge pull request #270 from dmunozv04/playback-speed
Add support for different playback speeds
2025-03-10 13:05:02 +01:00
David
b3f07b9a9d Merge branch 'main' into playback-speed 2025-03-10 13:02:29 +01:00
pre-commit-ci[bot]
5d20ca642b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-10 12:00:07 +00:00
dmunozv04
02c78e8aeb Clean code
Fixes DeepSource warnings
2025-03-10 12:59:30 +01:00
dmunozv04
f15ba5d5a6 Remove cyclomatic complexity warning 2025-03-10 11:43:39 +01:00
pre-commit-ci[bot]
e451769a29 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-10 10:42:07 +00:00
dmunozv04
1ae4c3019b Rework conditions
Fixes Chained comparison PYL-R1716
2025-03-10 11:41:33 +01:00
pre-commit-ci[bot]
7a45284a50 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-10 10:35:44 +00:00
David
8bfd19696b Merge pull request #258 from dmunozv04/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-03-10 11:25:07 +01:00
pre-commit-ci[bot]
53d7405a9c [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.6 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.6...v0.9.9)
2025-03-10 11:24:02 +01:00
David
7d769a9f62 Merge pull request #262 from dmunozv04/dependabot/pip/rich-click-1.8.6
Bump rich-click from 1.8.5 to 1.8.6
2025-03-10 11:20:52 +01:00
dependabot[bot]
6250353cb2 Bump rich-click from 1.8.5 to 1.8.6
Bumps [rich-click](https://github.com/ewels/rich-click) from 1.8.5 to 1.8.6.
- [Release notes](https://github.com/ewels/rich-click/releases)
- [Changelog](https://github.com/ewels/rich-click/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ewels/rich-click/compare/v1.8.5...v1.8.6)

---
updated-dependencies:
- dependency-name: rich-click
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 11:20:43 +01:00
David
e2e3e78218 Merge pull request #261 from dmunozv04/dependabot/pip/aiohttp-3.11.13
Bump aiohttp from 3.11.12 to 3.11.13
2025-03-10 11:20:09 +01:00
dependabot[bot]
7b0cfc5e68 Bump aiohttp from 3.11.12 to 3.11.13
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.11.12 to 3.11.13.
- [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.11.12...v3.11.13)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 11:19:47 +01:00
David
e4125c48e6 Merge pull request #259 from dmunozv04/dependabot/pip/pyytlounge-2.2.1
Bump pyytlounge from 2.1.2 to 2.2.1
2025-03-10 11:19:16 +01:00
dependabot[bot]
dbe64edf88 Bump pyytlounge from 2.1.2 to 2.2.1
Bumps [pyytlounge](https://github.com/FabioGNR/pyytlounge) from 2.1.2 to 2.2.1.
- [Release notes](https://github.com/FabioGNR/pyytlounge/releases)
- [Changelog](https://github.com/FabioGNR/pyytlounge/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FabioGNR/pyytlounge/compare/v2.1.2...v2.2.1)

---
updated-dependencies:
- dependency-name: pyytlounge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 10:17:21 +00:00
dmunozv04
b4ccfb7e96 Add support for different playback speeds 2025-03-10 10:31:53 +01:00
David
0d3ff8a54c Merge pull request #269 from dmunozv04/improved-logging
Improve logging
2025-03-09 16:28:30 +01:00
pre-commit-ci[bot]
f58eaeec22 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-03-09 15:26:04 +00:00
dmunozv04
a37c272662 Improve logging 2025-03-09 16:25:50 +01:00
dmunozv04
e5a1686afb Revert "Improved logging"
This reverts commit fb927aaacf.
2025-03-09 16:22:15 +01:00
dmunozv04
fb927aaacf Improved logging 2025-03-09 16:21:12 +01:00
dmunozv04
f486fec0bd Bump version 2025-02-22 00:49:06 +01:00
David
54015cf455 Merge pull request #255 from dmunozv04/fix-add-device
Fix adding devices
2025-02-22 00:48:45 +01:00
pre-commit-ci[bot]
114326e34c [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-02-21 23:45:26 +00:00
dmunozv04
3797200825 Fix adding devices 2025-02-22 00:44:29 +01:00
dmunozv04
eafedb7cf7 Move minimum supported version to python 3.9 2025-02-21 10:16:24 +01:00
dmunozv04
1cf539be9a Bump version 2025-02-21 10:15:07 +01:00
David
9d74a9b3ce Merge pull request #77 from boltgolt/feature-device-name
Allow setting device name to custom string
2025-02-19 18:26:12 +01:00
dmunozv04
9b5ea2b243 Merge remote-tracking branch 'origin/main' into pr/boltgolt/77 2025-02-19 10:18:04 +01:00
David
112a4faa50 Merge pull request #239 from dmunozv04/dependabot/pip/aiohttp-3.11.12
Bump aiohttp from 3.9.5 to 3.11.12
2025-02-19 00:29:03 +01:00
dependabot[bot]
027a8d7ebc Bump aiohttp from 3.9.5 to 3.11.12
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-19 00:28:52 +01:00
David
a495cdf62e Merge pull request #247 from dmunozv04/dependabot/pip/pyytlounge-2.1.2
Bump pyytlounge from 2.1.1 to 2.1.2
2025-02-19 00:28:24 +01:00
dependabot[bot]
24f1612f20 Bump pyytlounge from 2.1.1 to 2.1.2
Bumps [pyytlounge](https://github.com/FabioGNR/pyytlounge) from 2.1.1 to 2.1.2.
- [Release notes](https://github.com/FabioGNR/pyytlounge/releases)
- [Changelog](https://github.com/FabioGNR/pyytlounge/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FabioGNR/pyytlounge/compare/v2.1.1...v2.1.2)

---
updated-dependencies:
- dependency-name: pyytlounge
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-19 00:28:03 +01:00
David
c979d280a1 Merge pull request #250 from dmunozv04/create-web-session-async 2025-02-19 00:17:34 +01:00
pre-commit-ci[bot]
45bc7ff6e7 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-02-18 23:16:48 +00:00
dmunozv04
251a94f147 Make main function asnyc 2025-02-19 00:16:17 +01:00
pre-commit-ci[bot]
0ba8f4c3c5 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-02-13 20:44:53 +00:00
boltgolt
2ebc821ed9 Rename proposed device_name setting to join_name 2025-02-13 21:43:35 +01:00
pre-commit-ci[bot]
060fe7af5d [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-02-13 20:42:18 +00:00
boltgolt
879116e873 Merge branch 'main' into feature-device-name 2025-02-13 21:41:06 +01:00
David
1914afa432 Merge pull request #242 from dmunozv04/remove-argparse-dep
Remove argparse dependency
2025-02-12 13:49:39 +01:00
dmunozv04
82459c8986 Remove argparse dependency
Fixes argparse dependency unnecessary #195
2025-02-12 13:48:34 +01:00
David
c7cb4e8282 Merge pull request #221 from dmunozv04/dependabot/pip/pyytlounge-2.1.1
Bump pyytlounge from 2.0.0 to 2.1.1
2025-02-12 13:44:29 +01:00
dmunozv04
7ea0b8642c Update ytlounge client
to be compatible with pyytlounge v2.1.1
2025-02-12 13:43:03 +01:00
dependabot[bot]
79fc9c066c Bump pyytlounge from 2.0.0 to 2.1.1
Bumps [pyytlounge](https://github.com/FabioGNR/pyytlounge) from 2.0.0 to 2.1.1.
- [Release notes](https://github.com/FabioGNR/pyytlounge/releases)
- [Changelog](https://github.com/FabioGNR/pyytlounge/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FabioGNR/pyytlounge/compare/v2.0.0...v2.1.1)

---
updated-dependencies:
- dependency-name: pyytlounge
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-12 12:29:42 +00:00
David
2b9e1f8bf4 Merge pull request #227 from dmunozv04/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-02-12 13:23:07 +01:00
pre-commit-ci[bot]
f5b183a679 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.6 → v0.9.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.6...v0.9.6)
- [github.com/igorshubovych/markdownlint-cli: v0.43.0 → v0.44.0](https://github.com/igorshubovych/markdownlint-cli/compare/v0.43.0...v0.44.0)
2025-02-12 13:22:46 +01:00
David
bececa5096 Merge pull request #234 from dmunozv04/dependabot/pip/textual-slider-0.2.0
Bump textual-slider from 0.1.2 to 0.2.0
2025-02-12 13:21:06 +01:00
dependabot[bot]
4589c612d1 Bump textual-slider from 0.1.2 to 0.2.0
Bumps [textual-slider](https://github.com/TomJGooding/textual-slider) from 0.1.2 to 0.2.0.
- [Changelog](https://github.com/TomJGooding/textual-slider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TomJGooding/textual-slider/compare/v0.1.2...v0.2.0)

---
updated-dependencies:
- dependency-name: textual-slider
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-12 12:17:23 +00:00
David
26d1d7e481 Merge pull request #230 from dmunozv04/dependabot/pip/textual-1.0.0
Bump textual from 0.58.0 to 1.0.0
2025-02-12 13:16:20 +01:00
dmunozv04
73b277706e Make deepsource happy 2025-02-12 13:09:05 +01:00
pre-commit-ci[bot]
5884844da1 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-02-12 12:06:38 +00:00
dmunozv04
38efe843de Update textual styles to work with textual 1.0 2025-02-12 13:06:11 +01:00
dependabot[bot]
ccfcd00aa9 Bump textual from 0.58.0 to 1.0.0
Bumps [textual](https://github.com/Textualize/textual) from 0.58.0 to 1.0.0.
- [Release notes](https://github.com/Textualize/textual/releases)
- [Changelog](https://github.com/Textualize/textual/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Textualize/textual/compare/v0.58.0...v1.0.0)

---
updated-dependencies:
- dependency-name: textual
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-12 11:14:22 +00:00
David
e34dbb0f80 Merge pull request #241 from dmunozv04/aiohttp-3.11-support
Aiohttp-3.11-support
2025-02-12 12:00:42 +01:00
dmunozv04
ae6da834e4 Fix #233 2025-02-12 11:42:38 +01:00
dmunozv04
7e80b41bbb Prepare for aiohttp 3.11 2025-02-12 11:42:16 +01:00
David
3c66dc3607 Merge pull request #232 from dmunozv04/dependabot/docker/python-3.13-alpine3.21
Bump python from 3.11-alpine3.21 to 3.13-alpine3.21
2025-01-30 11:33:56 +01:00
David
0c8427edc0 Update python3.11 references to 3.13 2025-01-30 10:14:27 +01:00
dependabot[bot]
e637e514b2 Bump python from 3.11-alpine3.21 to 3.13-alpine3.21
Bumps python from 3.11-alpine3.21 to 3.13-alpine3.21.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-30 09:12:47 +00:00
David
c8b9fe157b Merge pull request #237 from dmunozv04/improve-exceptions
Improve exceptions
2025-01-30 10:08:13 +01:00
dmunozv04
dca9186d8b migrate to github actions cache 2025-01-24 16:56:31 +01:00
dmunozv04
adc7e1efe9 Pass markdownlint 2025-01-24 16:52:21 +01:00
dmunozv04
b96014840a ignore license on markdownlint 2025-01-24 16:47:26 +01:00
dmunozv04
147004e257 Remove unused variable 2025-01-24 16:44:37 +01:00
dmunozv04
6f4c27c0a5 Improve exceptions 2025-01-24 16:43:39 +01:00
David
f75dff8faf Merge pull request #229 from dmunozv04/dependabot/pip/xmltodict-0.14.2
Bump xmltodict from 0.13.0 to 0.14.2
2025-01-20 23:29:21 +01:00
dependabot[bot]
0e77418c2b Bump xmltodict from 0.13.0 to 0.14.2
Bumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.13.0 to 0.14.2.
- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)
- [Commits](https://github.com/martinblech/xmltodict/compare/v0.13.0...v0.14.2)

---
updated-dependencies:
- dependency-name: xmltodict
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 20:21:03 +00:00
David
f1d1787511 Merge pull request #222 from dmunozv04/dependabot/pip/rich-click-1.8.5
Bump rich-click from 1.8.3 to 1.8.5
2025-01-16 19:17:26 +01:00
dependabot[bot]
49b1b902d3 Bump rich-click from 1.8.3 to 1.8.5
Bumps [rich-click](https://github.com/ewels/rich-click) from 1.8.3 to 1.8.5.
- [Release notes](https://github.com/ewels/rich-click/releases)
- [Changelog](https://github.com/ewels/rich-click/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ewels/rich-click/compare/v1.8.3...v1.8.5)

---
updated-dependencies:
- dependency-name: rich-click
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-16 18:09:22 +00:00
David
5116e6c1e0 Merge pull request #224 from dmunozv04/dependabot/pip/rich-13.9.4
Bump rich from 13.7.1 to 13.9.4
2025-01-16 19:08:00 +01:00
dependabot[bot]
0b0a235046 Bump rich from 13.7.1 to 13.9.4
Bumps [rich](https://github.com/Textualize/rich) from 13.7.1 to 13.9.4.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v13.7.1...v13.9.4)

---
updated-dependencies:
- dependency-name: rich
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-16 18:06:02 +00:00
David
4eaebd3006 Merge pull request #223 from dmunozv04/dependabot/docker/3.11-alpine3.21
Bump python from 3.11-alpine3.19 to 3.11-alpine3.21
2025-01-16 19:04:11 +01:00
David
58e10f0f82 Update base image to 3.11-alpine3.21 2025-01-16 18:59:34 +01:00
dependabot[bot]
a93eeaa1cf Bump python from 3.11-alpine3.19 to 3.13-alpine3.19
Bumps python from 3.11-alpine3.19 to 3.13-alpine3.19.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-16 17:58:37 +00:00
David
77527ce4d5 Merge pull request #225 from dmunozv04/dependabot/pip/textual-slider-0.1.2
Bump textual-slider from 0.1.1 to 0.1.2
2025-01-16 18:56:40 +01:00
dmunozv04
6825ac6629 fix slider 2025-01-16 18:56:17 +01:00
dependabot[bot]
b59135316e Bump textual-slider from 0.1.1 to 0.1.2
Bumps [textual-slider](https://github.com/TomJGooding/textual-slider) from 0.1.1 to 0.1.2.
- [Changelog](https://github.com/TomJGooding/textual-slider/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TomJGooding/textual-slider/commits)

---
updated-dependencies:
- dependency-name: textual-slider
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-16 16:59:21 +00:00
David
7b17d5a2da Merge pull request #220 from dmunozv04/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2025-01-11 11:39:39 +01:00
dependabot[bot]
7e9f53e175 Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-11 10:35:05 +00:00
David
84c22c2dde Merge pull request #219 from dmunozv04:add-dependabot
Add dependabot.yml
2025-01-09 15:04:33 +01:00
dmunozv04
a7fbcd3dd2 add dependabot.yml 2025-01-09 14:59:38 +01:00
David
dbf5e3ac1c Merge pull request #217 from dmunozv04/close-properly-part-2
Improve exit logic
2025-01-09 14:21:12 +01:00
David
23f65125e2 Merge branch 'main' into close-properly-part-2 2025-01-06 22:17:01 +01:00
David
4feecaa570 Merge pull request #218 from dmunozv04/improve-docker-image-creation
Improve docker image creation (now supports forks)
2025-01-06 19:23:34 +01:00
dmunozv04
52a3f238d6 Also build images on forks 2025-01-06 19:16:17 +01:00
David
afff2a44b9 Merge branch 'main' into close-properly-part-2 2025-01-06 19:09:58 +01:00
pre-commit-ci[bot]
15c165d89a [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-01-06 18:09:46 +00:00
David
e71598599a Merge pull request #215 from dmunozv04/pyapp-tests
Fix CI package builds
2025-01-06 19:09:04 +01:00
David
760970a751 Merge branch 'main' into pyapp-tests 2025-01-06 19:08:44 +01:00
David
fb598809da Merge pull request #216 from dmunozv04/update-pre-commit-hooks
Update .pre-commit-config.yaml
2025-01-06 19:08:23 +01:00
pre-commit-ci[bot]
35d13373f9 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-01-06 18:06:32 +00:00
David
0145b3ba8d fix indent 2025-01-06 19:06:26 +01:00
David
8fdd13da04 Update .pre-commit-config.yaml
Replace isort, black and pycln with ruff
2025-01-06 19:04:36 +01:00
David
6dcc12baed Merge pull request #131 from dmunozv04/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-01-06 18:52:06 +01:00
pre-commit-ci[bot]
afaced8f84 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-01-06 17:47:30 +00:00
dmunozv04
b3dd27748b better actions name 2025-01-06 18:35:01 +01:00
dmunozv04
73192a1171 add v1 variant for linux amd64 builds 2025-01-06 18:31:16 +01:00
David
c7dc54fd66 Merge pull request #214 from dmunozv04/update-readme
Add badges to README.md
2025-01-06 18:25:52 +01:00
dmunozv04
edca2bf11a Improve exit logic 2025-01-06 18:23:46 +01:00
pre-commit-ci[bot]
f3d3bdd432 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-01-06 17:20:40 +00:00
dmunozv04
67eb40ca9d Add badges to README.md 2025-01-06 18:20:16 +01:00
David
284b7a1d2f Merge pull request #207 from dmunozv04:close-properly
improve exit logic
2024-12-23 23:01:29 +01:00
dmunozv04
ec109e0f10 Fix x86_64 macOS and test aarch64-windows 2024-12-15 14:00:46 +01:00
David
658fad64eb Update pyapp 2024-12-15 02:33:04 +01:00
dmunozv04
7880e222b0 change python cpu variant in build 2024-12-15 02:09:08 +01:00
dmunozv04
dcf53dcca9 improve exit logic 2024-12-15 02:08:07 +01:00
pre-commit-ci[bot]
1a271c567a [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-10-08 00:26:21 +00:00
pre-commit-ci[bot]
4473fc925e [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v5.0.0)
- [github.com/pycqa/isort: 5.12.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.12.0...5.13.2)
- [github.com/psf/black: 23.1.0 → 24.10.0](https://github.com/psf/black/compare/23.1.0...24.10.0)
- [github.com/hadialqattan/pycln: v2.3.0 → v2.4.0](https://github.com/hadialqattan/pycln/compare/v2.3.0...v2.4.0)
2024-10-08 00:26:04 +00:00
dmunozv04
d9ab2cd070 bump version 2024-09-18 17:20:27 +02:00
dmunozv04
ea2004ba94 fix release ci 2024-09-18 17:20:22 +02:00
David
63f5a3bc41 Bump version 2024-09-18 15:17:29 +02:00
David
e999a93503 Merge pull request #191 from dmunozv04/add-mutex-command
Implements mutex when sending commands to YouTube
2024-09-18 15:17:07 +02:00
pre-commit-ci[bot]
8cc3f8aa05 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-09-15 13:20:36 +00:00
dmunozv04
5fadc81a69 Fix occasional IndexError
in loungeScreenDisconnected event
2024-09-15 14:49:39 +02:00
dmunozv04
39aef5babf Test wrap command function in a mutex
to avoid race conditions with the _command_offset
2024-09-14 23:44:32 +02:00
David
c56cbfe095 Merge pull request #185 from dmunozv04/build-binaries
Build binaries
2024-08-18 17:30:34 +02:00
pre-commit-ci[bot]
bde4ecb72f [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-08-18 15:23:15 +00:00
dmunozv04
167383dea8 revert 2024-08-18 16:11:54 +02:00
dmunozv04
fb3c40d477 also build armv7 2024-08-18 15:58:15 +02:00
dmunozv04
cb738965a7 run on all events (and only publish on release) 2024-08-18 15:07:35 +02:00
dmunozv04
9ad335793a rename prefix 2024-08-18 14:35:02 +02:00
dmunozv04
fd400d077a update upload-artifact to v4 2024-08-18 14:19:39 +02:00
dmunozv04
f9c7b58ece add needs 2024-08-18 14:12:49 +02:00
dmunozv04
464baa7c59 trigger on test branch 2024-08-18 14:07:47 +02:00
dmunozv04
d9986e52b3 Merge remote-tracking branch 'origin/main' into build-binaries 2024-08-18 14:07:19 +02:00
dmunozv04
547a47b9ec modify action to create and publish binaries 2024-08-18 14:04:34 +02:00
dmunozv04
87d0e0e32e rework cli 2024-08-18 14:04:04 +02:00
David
854cb2462f Merge pull request #171 from guoard/dockerfile-issues
Fix buildx warnings when creating the docker image
2024-08-16 12:42:41 +02:00
David
662b71fc00 Merge pull request #177 from AN1MATEK/patch-1
Update config.json.template to correct the syntax error in auto_play
2024-07-14 11:12:08 +02:00
ANIMATEK
fd6b7cb43a Update config.json.template
Correct the error syntax from `autoplay` to the actual variable `auto_play`
2024-07-14 11:09:41 +02:00
dmunozv04
d17e59bf0d bump version 2024-07-07 17:36:01 +02:00
Ali Afsharzadeh
5bc6382f89 Fix buildx warnings when creating the docker image 2024-06-29 09:28:24 +03:30
David
205191f442 Merge pull request #150 from ryankupk/update-cli
Refactor CLI setup script, add prompts for muting and skipping native youtube ads
2024-06-21 17:08:47 +02:00
dmunozv04
810cd5eec3 change default options for autoplay and reporting, and mark default option 2024-06-21 17:08:15 +02:00
pre-commit-ci[bot]
e2ace8629f [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-06-21 15:01:56 +00:00
dmunozv04
e54ead26d2 Merge remote-tracking branch 'origin' into pr/ryankupk/150 2024-06-21 17:01:45 +02:00
David
49fba2f28f Merge pull request #161 from Ravioli8235/autoplay
Implement autoplay on/off toggle
2024-06-21 16:56:57 +02:00
pre-commit-ci[bot]
b1333a2f61 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-06-21 14:29:35 +00:00
dmunozv04
cfef219d32 fix autoplay padding 2024-06-21 16:29:17 +02:00
Ravioli8235
338e0479ba Merge remote-tracking branch 'origin/autoplay' into autoplay 2024-06-21 15:58:28 +02:00
Ravioli8235
bfefa94a7b fix panel fill 2024-06-21 15:58:15 +02:00
David
783e3d4240 Merge branch 'main' into autoplay 2024-06-17 14:55:07 +02:00
David
015f5a79c9 Merge pull request #168 from Shraymonks/fix-docker-root
Fix deps permissions in docker build
2024-06-16 14:22:25 +02:00
Raymond Ha
dc72db0609 Fix deps permissions in docker build 2024-06-05 16:18:10 -07:00
David
8de38cc92b Merge pull request #167 from dmunozv04/update-actions
Update actions versions
2024-06-05 19:29:42 +02:00
dmunozv04
94ba642af1 update actions versions to latest 2024-06-05 19:27:42 +02:00
dmunozv04
6e09db9994 bump version 2024-05-31 15:35:16 +02:00
David
b56d7443d1 Merge pull request #163 from dmunozv04/refresh-auth-after-disconnect
Refresh auth after every disconnect
2024-05-31 15:34:40 +02:00
dmunozv04
e92ba897c4 Refresh auth after every disconnect, ensuring there's a fresh token. Fixes Shorts aren't handled anymore #162
and maybe changes something related to Disable Autoplay #82
2024-05-31 15:26:58 +02:00
dmunozv04
5214190fd0 revert last commit 2024-05-30 12:35:59 +02:00
David
d3341009a6 Merge branch 'main' into autoplay 2024-05-30 12:27:47 +02:00
dmunozv04
adc0f5b95d enable building PR docker images 2024-05-30 12:26:55 +02:00
David
5dbd16ddd5 Merge branch 'main' into autoplay 2024-05-30 09:05:18 +02:00
pre-commit-ci[bot]
faa0379b89 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-05-30 07:03:27 +00:00
Ravioli8235
fb3ed6b39a Implement autoplay 2024-05-30 08:53:35 +02:00
dmunozv04
1ab7e73b52 bump version 2024-05-29 23:41:42 +02:00
David
d310e4c817 Merge pull request #160 from dmunozv04/update-dependencies
Update dependencies
2024-05-29 23:39:01 +02:00
pre-commit-ci[bot]
d21bb6320f [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-05-29 21:31:56 +00:00
dmunozv04
dd42e20dc4 Remove session closer, it seems to break the config setup 2024-05-05 19:16:27 +02:00
dmunozv04
213ae97bf2 Fix web_session 2024-05-05 19:07:03 +02:00
pre-commit-ci[bot]
865f5469a2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-05-03 16:24:21 +00:00
Ryan Kupka
daa7026221 Refactor CLI setup script, add prompts for muting and skipping native youtube ads 2024-05-03 10:18:16 -06:00
dmunozv04
582b9bf725 Patch the main aiohttp.ClientSession() into YTlounge 2024-04-27 19:26:55 +02:00
dmunozv04
ce95b6dbf0 Update dependencies 2024-04-27 19:17:52 +02:00
dmunozv04
80196b19aa Rework dockerfile and build for armv7 2024-03-12 22:40:23 +01:00
David
4dd6aa1c4d Merge pull request #140 from SShah7433/utilize_logging_standards
Update logging for standards
2024-03-07 09:14:40 +01:00
David
1a5f29fe2a Merge branch 'main' into utilize_logging_standards 2024-03-07 09:14:33 +01:00
David
13fe1f69ae Bump version 2024-02-14 22:07:51 +01:00
David
3b1ce5297f Merge pull request #136 from PetkoVasilev/main
fix for overlapping segments
2024-02-12 09:07:40 +01:00
Sidd Shah
e689a713ef remove unused code from prev logging implementation 2024-02-02 22:34:01 -05:00
Sidd Shah
e6b1e14d80 remove unused argument 2024-02-02 22:30:58 -05:00
pre-commit-ci[bot]
4934eff8b7 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-02-03 03:27:03 +00:00
Sidd Shah
674a13e43a utilize standard logging libraries and formats 2024-02-02 22:22:08 -05:00
David
152ba104a6 Merge branch 'main' into main 2024-01-31 11:47:32 +01:00
dmunozv04
265c56f3d6 bump version 2024-01-29 22:28:07 +01:00
David
7e954478f2 Merge branch 'main' into main 2024-01-29 21:47:07 +01:00
David
4f39f64ed0 Merge pull request #134 from bertybuttface/patch-5
Fix list_to_tuple
2024-01-29 21:46:48 +01:00
Petko Vasilev
8208a51176 more understandable logic (same result) 2024-01-29 12:39:22 +02:00
pre-commit-ci[bot]
ab6b67f88b [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-01-28 08:11:55 +00:00
Petko Vasilev
9068b58bf6 fix for overlapping segments 2024-01-28 09:59:47 +02:00
bertybuttface
a9f9a5b31c Update api_helpers.py
Fix list to tuple
2024-01-27 16:36:31 +00:00
David
a75dd83548 Merge pull request #132 from dmunozv04/open-file-utf-8
open and close config file with
2024-01-17 14:25:04 +01:00
dmunozv04
4e3c9d115c open and close config file with
utf-8 encoding
2024-01-15 21:53:08 +01:00
David
04533162cb Merge pull request #129 from dmunozv04/make-docker-image-smaller
Make docker image smaller by caching .pyc and removing .py files
2024-01-14 21:11:41 +01:00
pre-commit-ci[bot]
bee2a9c80f [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-01-14 15:39:02 +00:00
dmunozv04
f620ed2fcc Make docker image smaller by caching .pyc and removing .py files 2024-01-14 16:38:39 +01:00
David
db26bff3d2 Merge pull request #128 from dmunozv04/deepsource-autofix-c896b47c
refactor: refactor unnecessary `else` / `elif` when `if` block has a `continue` statement
2024-01-14 15:19:53 +01:00
David
d205848132 Merge pull request #127 from dmunozv04/deepsource-autofix-75d7989d
refactor: replace range(len(...)) with enumerate(...)
2024-01-14 15:19:22 +01:00
David
29445e678f Merge pull request #126 from dmunozv04/deepsource-autofix-097a200c
refactor: refactor unnecessary `else` / `elif` when `if` block has a `return` statement
2024-01-14 15:19:04 +01:00
deepsource-autofix[bot]
35453bc49e refactor: refactor unnecessary else / elif when if block has a continue statement
The use of `else` or `elif` becomes redundant and can be dropped if the last statement under the leading `if` / `elif` block is a `continue` statement.
In the case of an `elif` after `continue`, it can be written as a separate `if` block.
For `else` blocks after `continue`, the statements can be shifted out of `else`. Please refer to the examples below for reference.

Refactoring the code this way can improve code-readability and make it easier to maintain.
2024-01-14 14:18:37 +00:00
deepsource-autofix[bot]
88875a82d3 refactor: replace range(len(...)) with enumerate(...)
Using `range(len(...))` is not pythonic. Python does not have not index-based loops. Instead, it uses collection iterators.  Python has a built-in method `enumerate` which adds a counter to an iterable.
2024-01-14 14:18:20 +00:00
deepsource-autofix[bot]
446393b078 refactor: refactor unnecessary else / elif when if block has a return statement
The use of `else` or `elif` becomes redundant and can be dropped if the last statement under the leading `if` / `elif` block is a `return` statement.
In the case of an `elif` after `return`, it can be written as a separate `if` block.
For `else` blocks after `return`, the statements can be shifted out of `else`. Please refer to the examples below for reference.

Refactoring the code this way can improve code-readability and make it easier to maintain.
2024-01-14 14:18:05 +00:00
David
784d54c4e2 Merge pull request #125 from dmunozv04/add-pre-commit
add pre-commit
2024-01-14 15:12:02 +01:00
pre-commit-ci[bot]
66b39c4cac [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-01-14 14:10:15 +00:00
dmunozv04
321a9e6e9b reformat DIAL license 2024-01-14 15:09:30 +01:00
pre-commit-ci[bot]
ecb3583c35 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-01-14 14:03:37 +00:00
dmunozv04
3cee674e91 Merge remote-tracking branch 'origin/add-pre-commit' into add-pre-commit 2024-01-14 15:03:17 +01:00
dmunozv04
d575a296e7 setup black --preview 2024-01-14 15:03:01 +01:00
pre-commit-ci[bot]
7bf52b6df1 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-01-14 13:53:04 +00:00
dmunozv04
846ea444d2 Merge remote-tracking branch 'origin/add-pre-commit' into add-pre-commit 2024-01-14 14:52:56 +01:00
dmunozv04
1671d7841b set black for max line length 2024-01-14 14:52:38 +01:00
pre-commit-ci[bot]
a4f0b5fffe [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-01-14 13:47:14 +00:00
dmunozv04
dae27e7aa3 add pre-commit 2024-01-14 14:46:46 +01:00
David
5df90a234d Merge pull request #122 from dmunozv04/improve-logging-and-shorts-hack
Improve logging and shorts hack
2024-01-14 14:41:12 +01:00
dmunozv04
c3fd67df27 Clean code and fix #121 2023-12-29 16:19:44 +01:00
dmunozv04
35652b6247 Improve exit behaviour 2023-12-29 15:39:41 +01:00
dmunozv04
8ab9cf9519 Improve logging and add hack to view
one short
2023-12-26 19:34:24 +01:00
dmunozv04
f4fbbcdff5 Remove connect fix now that it's been pushed upstream 2023-12-19 08:24:45 +01:00
David
385ed8268c Update package version 2023-12-09 19:22:45 +01:00
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
boltgolt
4de056d2a8 Allow setting device name to custom string 2023-10-17 17:55:36 +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
David
d7a83035a6 Update main.yml
Run on a release
2023-10-13 20:09:50 +02:00
David
9d7d9665dc Merge pull request #57 from dmunozv04/v2-test
V2
2023-10-13 20:07:28 +02:00
dmunozv04
55f2798adc Inform that v2 has been released and requires changes 2023-10-13 19:54:11 +02:00
dmunozv04
907e0ec549 Pin dependencies 2023-10-13 17:57:14 +02:00
dmunozv04
c83e406c8f Fix GitHub actions 2023-10-13 17:50:29 +02:00
dmunozv04
143e5e4eff Final v2 commit before launch?
Adds better logging and modifies README.md
2023-10-13 17:46:25 +02:00
dmunozv04
aad6eea686 ArmV7 needs more work, PR welcome 2023-09-04 14:31:13 +02:00
dmunozv04
fc309102df Forgot to remove miniaudio dep 2023-09-04 14:21:23 +02:00
dmunozv04
359a7f7be1 first v2 commit 2023-09-04 14:11:34 +02:00
David
1aa06e677f Merge pull request #50 from themagic314/temp-miniaudio-fix
temp fix for miniaudio
2023-05-31 17:28:15 +02:00
Eli
6c879b1649 temp fix for miniaudio 2023-05-30 19:29:42 +04:00
David
b98a00a43e Merge pull request #49 from Shraymonks/tvOS-detect
Check tvOS instead of specific atv models
2023-05-28 18:31:00 +02:00
Raymond Ha
4ec4f7c101 Use f-string 2023-05-26 01:24:08 -07:00
Raymond Ha
49cd69de8b Combine ifs 2023-05-26 01:14:53 -07:00
Raymond Ha
230024c179 Check tvOS instead of specific atv models 2023-05-25 22:03:23 -07:00
David
f06e2b1490 Update README.md 2023-05-09 10:47:58 +02:00
David
3b0cbff054 Update config_setup.py
fixes #45
2023-04-23 09:48:56 +02:00
David
067d429d33 Update config_setup.py
Move loop creation on config to main
2023-04-23 00:48:13 +02:00
David
ce82c7b719 Merge pull request #44 from oxixes/channel_whitelist
Added channel whitelists and general fixes
2023-04-22 19:56:44 +02:00
oxixes
42c09ef588 Last fixes 2023-04-22 19:50:37 +02:00
oxixes
890c3956de Remove .DS_Store 2023-04-22 19:15:36 +02:00
oxixes
224d00174b I'm dumb, whatever, fixed 2023-04-22 19:09:37 +02:00
oxixes
3f8e24c8a4 Improved README 2023-04-22 13:55:32 +02:00
oxixes
eb51963da9 Merge branch 'channel_whitelist' of https://github.com/oxixes/iSponsorBlockTV into channel_whitelist 2023-04-22 13:43:15 +02:00
oxixes
975b2690a4 Added channel whitelist 2023-04-22 13:40:11 +02:00
oxixes
77e710849f Merge branch 'dmunozv04:main' into channel_whitelist 2023-04-22 13:22:09 +02:00
oxixes
73d1a024f3 Added channels whitelist 2023-04-22 13:20:10 +02:00
David
ab10c6d321 Merge pull request #42 from oxixes/comma_fix
Allow comma separated categories
2023-04-17 10:24:29 +02:00
oxixes
e0c0548496 Allow comma separated categories 2023-04-17 10:15:56 +02:00
David
a665f1cd27 Merge pull request #41 from bertybuttface/patch-2
Add projects using this project
2023-04-16 18:26:48 +02:00
bertybuttface
58ac0c1338 Update README.md 2023-04-16 16:43:53 +01:00
David
e2adf09db8 Merge pull request #39 from bertybuttface/patch-1
Update main.py
2023-04-16 17:17:46 +02:00
bertybuttface
8edf4859ab Update main.py
remove unused code
2023-03-28 22:13:20 +01:00
David
e5a795edab Merge pull request #37 from dmunozv04/not-build-armv7
remove armv7 docker build
2023-01-16 22:04:20 +01:00
David
fae6134295 Update README.md 2023-01-16 22:01:22 +01:00
David
5658dd7537 Merge branch 'main' into not-build-armv7 2023-01-16 22:00:45 +01:00
David
0413d612bb Fix naming for category setup
Previous naming could imply that a comma was required between categories
2023-01-08 15:12:46 +01:00
David
fe606292f8 fix PR docker builds 2022-11-14 00:26:31 +01:00
David
b9c41aa022 push docker containers in PRs 2022-11-14 00:22:42 +01:00
David
60d9a86413 remove armv7 docker build 2022-11-13 23:42:47 +01:00
DeepSource Bot
933ee36a18 Add .deepsource.toml 2022-07-25 16:12:43 +00:00
David
7c207c5b0a Merge pull request #23 from dmunozv04:Sending-hashed-video-ids
General fixes
2022-04-22 14:46:14 +02:00
dmunozv04
816b4ec9ef Fix the docker bug #22
Make video ids hashed
Get 10 videos from youtube to make sure that the right id is found
Refactored the api calls
2022-04-22 14:45:32 +02:00
David
0be5e2d4cd Update README.md 2022-04-12 00:24:26 +02:00
David
781c0d4a73 Update Dockerfile 2022-04-12 00:07:42 +02:00
41 changed files with 3430 additions and 584 deletions

BIN
.DS_Store vendored

Binary file not shown.

9
.deepsource.toml Normal file
View File

@@ -0,0 +1,9 @@
version = 1
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"
max_line_length = 100

9
.dockerignore Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,82 @@
name: build docker images
# Controls when the workflow will run
on:
push:
branches:
- '*'
tags:
- 'v*.*.*'
- 'v*.*.*-*'
pull_request:
branches:
- '*'
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
runs-on: ubuntu-latest
steps:
# Get the repository's code
- name: Checkout
uses: actions/checkout@v5
# Generate docker tags
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
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
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}
type=semver,pattern=v{{major}}.{{minor}}
type=ref,event=branch
type=schedule
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
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@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request' && env.DOCKERHUB_USERNAME != ''
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@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/386, linux/arm/v6
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,72 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: build docker images
# Controls when the workflow will run
on:
push:
branches:
- 'main'
tags:
- 'v*'
pull_request:
branches:
- 'main'
workflow_dispatch:
permissions:
contents: read
packages: write
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
runs-on: ubuntu-latest
steps:
# Get the repository's code
- name: Checkout
uses: actions/checkout@v2
# Generate docker tags
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: ghcr.io/dmunozv04/isponsorblocktv, dmunozv04/isponsorblocktv
tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
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
cache-to: type=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache,mode=max

211
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,211 @@
# 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:
- 'main'
tags:
- 'v*'
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@v5
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v6
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:
permissions:
id-token: write
attestations: write
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-24.04-arm
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.28.0
steps:
- name: Checkout
uses: actions/checkout@v5
- 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@v6
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@v5
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: Attest build provenance
uses: actions/attest-build-provenance@v3
with:
subject-path: dist/binary/*
continue-on-error: true # Continue if attestation fails (it will fail on forks)
- 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@v5
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@v5
name: Get artifact
with:
path: dist
merge-multiple: true
- name: Add assets to release
uses: softprops/action-gh-release@v2
with:
files: dist/*

View File

@@ -18,12 +18,13 @@ jobs:
steps:
# Get the repository's code
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v5
# Update description
- name: Update repo description
uses: peter-evans/dockerhub-description@v2
uses: peter-evans/dockerhub-description@v5
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 }}

11
.gitignore vendored
View File

@@ -35,6 +35,9 @@ MANIFEST
pip-log.txt
pip-delete-this-directory.txt
# macOS
*.DS_Store
# Unit test / coverage reports
htmlcov/
.tox/
@@ -151,5 +154,9 @@ cython_debug/
#.idea/
#config folder
config/
config.json
data/
data/config.json
.DS_Store
.DS_Store

1
.markdownlintignore Normal file
View File

@@ -0,0 +1 @@
LICENSE.md

31
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,31 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
# Inspired by textual pre-commit config
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.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
- id: check-case-conflict # checks for files that would conflict in case-insensitive filesystems
- id: check-merge-conflict # checks for files that contain merge conflict strings
- id: check-json # checks json files for parseable syntax
- id: check-toml # checks toml files for parseable syntax
- id: check-yaml # checks yaml files for parseable syntax
args: [ '--unsafe' ] # Instead of loading the files, parse them for syntax.
- id: check-shebang-scripts-are-executable # ensures that (non-binary) files with a shebang are executable
- id: check-vcs-permalinks # ensures that links to vcs websites are permalinks
- 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/astral-sh/ruff-pre-commit
rev: v0.12.12
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
- id: ruff-format
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.45.0
hooks:
- id: markdownlint
args: ["--fix"]

View File

@@ -1,24 +1,38 @@
# syntax=docker/dockerfile:1
FROM python:3.13-alpine3.21 AS base
FROM python:alpine
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" PIP_NO_CACHE_DIR=off iSPBTV_docker=True
COPY requirements.txt .
RUN apk add gcc musl-dev build-base linux-headers libffi-dev rust cargo openssl-dev git avahi && \
pip install --upgrade pip setuptools-rust wheel && \
pip install -r requirements.txt && \
apk del gcc musl-dev build-base linux-headers libffi-dev rust cargo openssl-dev git && \
rm -rf /root/.cache /root/.cargo
COPY requirements.txt .
FROM base AS compiler
WORKDIR /app
COPY *.py .
COPY src .
ENTRYPOINT ["/opt/venv/bin/python3", "-u", "main.py"]
RUN python3 -m compileall -b -f . && \
find . -name "*.py" -type f -delete
FROM base AS dep_installer
COPY requirements.txt .
RUN apk add --no-cache gcc musl-dev && \
pip install --upgrade pip wheel && \
pip install -r requirements.txt && \
pip uninstall -y pip wheel && \
apk del gcc musl-dev && \
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
ENV PIP_NO_CACHE_DIR=off iSPBTV_docker=True iSPBTV_data_dir=data TERM=xterm-256color COLORTERM=truecolor
COPY requirements.txt .
COPY --from=dep_installer /usr/local /usr/local
WORKDIR /app
COPY --from=compiler /app .
ENTRYPOINT ["python3", "-u", "main.pyc"]

View File

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

105
README.md
View File

@@ -1,68 +1,68 @@
# iSponsorBlockTV
Skip sponsor segments in YouTube videos playing on an Apple TV.
[![ghcr.io Pulls](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fipitio.github.io%2Fbackage%2Fdmunozv04%2FiSponsorBlockTV%2Fisponsorblocktv.json&query=downloads&logo=github&label=ghcr.io%20pulls&style=flat)](https://ghcr.io/dmunozv04/isponsorblocktv)
[![Docker Pulls](https://img.shields.io/docker/pulls/dmunozv04/isponsorblocktv?logo=docker&style=flat)](https://hub.docker.com/r/dmunozv04/isponsorblocktv/)
[![GitHub Release](https://img.shields.io/github/v/release/dmunozv04/isponsorblocktv?logo=GitHub&style=flat)](https://github.com/dmunozv04/iSponsorBlockTV/releases/latest)
[![GitHub Repo stars](https://img.shields.io/github/stars/dmunozv04/isponsorblocktv?style=flat)](https://github.com/dmunozv04/isponsorblocktv)
This project is written in asycronous python and should be pretty quick.
iSponsorBlockTV is a self-hosted application that connects to your YouTube TV
app (see compatibility below) and automatically skips segments (like Sponsors
or intros) in YouTube videos using the [SponsorBlock](https://sponsor.ajay.app/)
API. It can also auto mute and press the "Skip Ad" button the moment it becomes
available on YouTube ads.
# Installation
## Installation
## Docker
### Setup
Check the [wiki](https://github.com/dmunozv04/iSponsorBlockTV/wiki/Installation)
You need to set up several things before you can run the project.
Create blank config file: `touch config.json`
Run:
```sh
docker run --rm -it \
--network=host \
--entrypoint /opt/venv/bin/python3 \
-v /PATH_TO_YOUR_CONFIG.json:/app/config.json \
ghcr.io/dmunozv04/isponsorblocktv \
/app/create_config.py
```
## Run
```sh
docker pull ghcr.io/dmunozv04/isponsorblocktv
docker run -d \
--name iSponsorBlockTV \
--restart=unless-stopped \
--network=host \
-v /PATH_TO_YOUR_CONFIG.json:/app/config.json \
ghcr.io/dmunozv04/isponsorblocktv
```
## From source
## Compatibility
You need to install [python](https://www.python.org/downloads/) first, and to make it available in your PATH. After, clone the repo.
Then you need to download the dependencies with pip:
```python3 -m pip install -r requirements.txt```
Lastly, run ```main.py```
Legend: ✅ = Working, ❌ = Not working, ❔ = Not tested
### Setup
Open an issue/pull request if you have tested a device that isn't listed here.
You need to retrieve airplay keys to be able to connect to the Apple TV. (It will be made simpler in the future)
For now, use `atvremote`, a script included in pyatv:
1. atvremote scan
2. atvremote pair --protocol airplay --id `identifier you got on the previous step`
| Device | Status |
|:-------------------|:------:|
| Apple TV | ✅* |
| Samsung TV (Tizen) | ✅ |
| LG TV (WebOS) | ✅ |
| Android TV | ✅ |
| Chromecast | ✅ |
| Google TV | ✅ |
| Roku | ✅ |
| Fire TV | ✅ |
| CCwGTV | ✅ |
| Nintendo Switch | ✅ |
| Xbox One/Series | ✅ |
| Playstation 4/5 | ✅ |
Get [YouTube api key](https://developers.google.com/youtube/registering_an_application)
*Ad muting won't work when using AirPlay to send the audio to another speaker.
Edit config.json.template and save it as config.json
# Usage
## Usage
Run iSponsorBLockTV in the same network as the Apple TV.
Run iSponsorBlockTV on a computer that has network access. It doesn't need to
be on the same network as the device, only access to youtube.com is required.
It connect to the Apple TV, watch its activity and skip any sponsor segment using the [SponsorBlock](https://sponsor.ajay.app/) API.
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 TV application.
The last 5 videos' segments are cached to limit the number on queries on SponsorBlock and YouTube.
## Libraries used
- [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
- [ssdp](https://github.com/codingjoe/ssdp) Used for auto discovery
# Libraries used
- [pyatv](https://github.com/postlund/pyatv) Used to connect to the Apple TV
- [asyncio] and [aiohttp]
- [async_lru]
- [json]
## Projects using this project
# Contributing
- [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`)
@@ -72,7 +72,10 @@ The last 5 videos' segments are cached to limit the number on queries on Sponsor
## Contributors
- [dmunozv04](https://github.com/dmunozv04) - creator and maintainer
- [HaltCatchFire](https://github.com/HaltCatchFire) - updated dependencies and improved skip logic
# License
[![Contributors](https://contrib.rocks/image?repo=dmunozv04/iSponsorBlockTV)](https://github.com/dmunozv04/iSponsorBlockTV/graphs/contributors)
Made with [contrib.rocks](https://contrib.rocks).
## License
[![GNU GPLv3](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)

View File

@@ -1,7 +1,25 @@
{
"atvs": [
{"identifier": "", "airplay_credentials": ""}
"devices": [
{
"screen_id": "",
"name": "YouTube on TV",
"offset": 0
}
],
"apikey":"",
"skip_categories": ["sponsor"]
}
"skip_categories": [
"sponsor"
],
"skip_count_tracking": true,
"mute_ads": true,
"skip_ads": true,
"minimum_skip_length": 1,
"auto_play": true,
"join_name": "iSponsorBlockTV",
"apikey": "",
"channel_whitelist": [
{"id": "",
"name": ""
}
],
"use_proxy": false
}

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
services:
iSponsorBlockTV:
image: ghcr.io/dmunozv04/isponsorblocktv
container_name: iSponsorBlockTV
restart: unless-stopped
volumes:
- /PATH_TO_YOUR_DATA_DIR:/app/data

View File

@@ -1,100 +0,0 @@
import pyatv
import json
import asyncio
from pyatv.const import DeviceModel
import sys
def save_config(config, config_file):
with open(config_file, "w") as f:
json.dump(config, f)
#Taken from postlund/pyatv atvremote.py
async def _read_input(loop: asyncio.AbstractEventLoop, prompt: str):
sys.stdout.write(prompt)
sys.stdout.flush()
user_input = await loop.run_in_executor(None, sys.stdin.readline)
return user_input.strip()
async def find_atvs(loop):
devices = await pyatv.scan(loop)
if not devices:
print("No devices found")
return
atvs = []
for i in devices:
#Only get Apple TV's
if i.device_info.model in [DeviceModel.Gen4, DeviceModel.Gen4K, DeviceModel.AppleTV4KGen2]:
#if i.device_info.model in [DeviceModel.AppleTV4KGen2]: #FOR TESTING
if input("Found {}. Do you want to add it? (y/n) ".format(i.name)) == "y":
identifier = i.identifier
pairing = await pyatv.pair(i, loop=loop, protocol=pyatv.Protocol.AirPlay)
await pairing.begin()
if pairing.device_provides_pin:
pin = await _read_input(loop, "Enter PIN on screen: ")
pairing.pin(pin)
await pairing.finish()
if pairing.has_paired:
creds = pairing.service.credentials
atvs.append({"identifier": identifier, "airplay_credentials": creds})
print("Pairing successful")
await pairing.close()
return atvs
def main(config, config_file, debug):
try: num_atvs = len(config["atvs"])
except: num_atvs = 0
if input("Found {} Apple TV(s) in config.json. Add more? (y/n) ".format(num_atvs)) == "y":
loop = asyncio.get_event_loop_policy().get_event_loop()
if debug:
loop.set_debug(True)
asyncio.set_event_loop(loop)
task = loop.create_task(find_atvs(loop))
loop.run_until_complete(task)
atvs = task.result()
try:
for i in atvs:
config["atvs"].append(i)
print("done adding")
except:
print("rewriting atvs (don't worry if none were saved before)")
config["atvs"] = atvs
try : apikey = config["apikey"]
except:
apikey = ""
if apikey != "" :
if input("Apikey already specified. Change it? (y/n) ") == "y":
apikey = input("Enter your API key: ")
config["apikey"] = apikey
else:
print("get youtube apikey here: https://developers.google.com/youtube/registering_an_application")
apikey = input("Enter your API key: ")
config["apikey"] = apikey
try: skip_categories = config["skip_categories"]
except:
skip_categories = []
if skip_categories != []:
if input("Skip categories already specified. Change them? (y/n) ") == "y":
categories = input("Enter skip categories (space sepparated) Options: [sponsor, selfpromo, exclusive_access, interaction, poi_highlight, intro, outro, preview, filler, music_offtopic:\n")
skip_categories = categories.split(" ")
else:
categories = input("Enter skip categories (space sepparated) Options: [sponsor, selfpromo, exclusive_access, interaction, poi_highlight, intro, outro, preview, filler, music_offtopic:\n")
skip_categories = categories.split(" ")
config["skip_categories"] = skip_categories
print("config finished")
save_config(config, config_file)
if __name__ == "__main__":
print("starting")
main()

View File

@@ -1,46 +0,0 @@
import argparse
from . import config_setup
from . import main
from . import macos_install
import json
import os
import logging
def load_config(config_file):
try:
with open(config_file) as f:
config = json.load(f)
except:
if os.getenv('iSPBTV_docker'):
print("You are running in docker, you have to mount the config file.\nPlease check the README.md for more information.")
else:
config = {} #Create blank config to setup
return config
def app_start():
parser = argparse.ArgumentParser(description='iSponsorblockTV')
parser.add_argument('--file', '-f', default='config.json', help='config file')
parser.add_argument('--setup', '-s', action='store_true', help='setup the program')
parser.add_argument('--debug', '-d', action='store_true', help='debug mode')
parser.add_argument('--macos_install', action='store_true', help='install in macOS')
args = parser.parse_args()
config = load_config(args.file)
if args.debug:
logging.basicConfig(level=logging.DEBUG)
if args.setup: #Setup the config file
config_setup.main(config, args.file, args.debug)
if args.macos_install:
macos_install.main()
else:
try: #Check if config file has the correct structure
config["atvs"], config["apikey"], config["skip_categories"]
except: #If not, ask to setup the program
print("invalid config file, please run with --setup")
os.exit()
main.main(config["atvs"], config["apikey"], config["skip_categories"], args.debug)
if __name__ == "__main__":
app_start()

View File

@@ -1,46 +0,0 @@
import plistlib
import os
from . import config_setup
default_plist = {"Label": "com.dmunozv04iSponsorBlockTV",
"RunAtLoad": True,
"StartInterval": 20,
"EnvironmentVariables": {"PYTHONUNBUFFERED": "YES"},
"StandardErrorPath": "", #Fill later
"StandardOutPath": "",
"ProgramArguments" : "",
"WorkingDirectory": ""
}
def create_plist(path):
plist = default_plist
plist["ProgramArguments"] = [path + "/iSponsorBlockTV-macos"]
plist["StandardErrorPath"] = path + "/iSponsorBlockTV.error.log"
plist["StandardOutPath"] = path + "/iSponsorBlockTV.out.log"
plist["WorkingDirectory"] = path
launchd_path = os.path.expanduser("~/Library/LaunchAgents/")
path_to_save = launchd_path + "com.dmunozv04.iSponsorBlockTV.plist"
with open(path_to_save, 'wb') as fp:
plistlib.dump(plist, fp)
def run_setup(file):
config = {}
config_setup.main(config, file, debug=False)
def main():
correct_path = os.path.expanduser("~/iSponsorBlockTV")
if os.path.isfile(correct_path + "/iSponsorBlockTV-macos"):
print("Program is on the right path")
print("The launch daemon will now be installed")
create_plist(correct_path)
run_setup(correct_path + "/config.json")
print("Launch daemon installed. Please restart the computer to enable it or use:\n launchctl load ~/Library/LaunchAgents/com.dmunozv04.iSponsorBlockTV.plist")
else:
if not os.path.exists(correct_path):
os.makedirs(correct_path)
print("Please move the program to the correct path: " + correct_path + "opeing now on finder...")
os.system("open -R " + correct_path)
if __name__ == "__main__":
main()

View File

@@ -1,172 +0,0 @@
import asyncio
import pyatv
import aiohttp
from cache import AsyncTTL
import time
import logging
def listToTuple(function):
def wrapper(*args):
args = [tuple(x) if type(x) == list else x for x in args]
result = function(*args)
result = tuple(result) if type(result) == list else result
return result
return wrapper
class MyPushListener(pyatv.interface.PushListener):
task = None
apikey = None
rc = None
web_session = None
categories = ["sponsor"]
def __init__(self, apikey, atv, categories, web_session):
self.apikey = apikey
self.rc = atv.remote_control
self.web_session = web_session
self.categories = categories
self.atv = atv
def playstatus_update(self, updater, playstatus):
logging.debug("Playstatus update" + str(playstatus))
try:
self.task.cancel()
except:
pass
time_start = time.time()
self.task = asyncio.create_task(process_playstatus(playstatus, self.apikey, self.rc, self.web_session, self.categories, self.atv, time_start))
def playstatus_error(self, updater, exception):
logging.error(exception)
print("stopped")
async def process_playstatus(playstatus, apikey, rc, web_session, categories, atv, time_start):
logging.debug("App playing is:" + str(atv.metadata.app.identifier))
if playstatus.device_state == playstatus.device_state.Playing and atv.metadata.app.identifier == "com.google.ios.youtube":
vid_id = await get_vid_id(playstatus.title, playstatus.artist, apikey, web_session)
print(vid_id)
segments, duration = await get_segments(vid_id, web_session, categories)
print(segments)
await time_to_segment(segments, playstatus.position, rc, time_start)
@AsyncTTL(time_to_live=300, maxsize=5)
async def get_vid_id(title, artist, api_key, web_session):
url = f"https://youtube.googleapis.com/youtube/v3/search?q={title} - {artist}&key={api_key}&maxResults=1"
async with web_session.get(url) as response:
response = await response.json()
vid_id = response["items"][0]["id"]["videoId"]
return vid_id
@listToTuple
@AsyncTTL(time_to_live=300, maxsize=5)
async def get_segments(vid_id, web_session, categories = ["sponsor"]):
params = {"videoID": vid_id,
"category": categories,
"actionType": "skip",
"service": "youtube"}
headers = {'Accept': 'application/json'}
url = "https://sponsor.ajay.app/api/skipSegments"
async with web_session.get(url, headers = headers, params = params) as response:
response = await response.json()
segments = []
try:
duration = response[0]["videoDuration"]
for i in response:
segment = i["segment"]
try:
#Get segment before to check if they are too close to each other
segment_before_end = segments[-1][1]
segment_before_start = segments[-1][0]
except:
segment_before_end = -10
if segment[0] - segment_before_end < 1: #Less than 1 second appart, combine them and skip them together
segment = [segment_before_start, segment[1]]
segments.pop()
segments.append(segment)
except:
duration = 0
return segments, duration
async def time_to_segment(segments, position, rc, time_start):
position = position + (time.time() - time_start)
for segment in segments:
if position < 2 and (position >= segment[0] and position < segment[1]):
next_segment = [position, segment[1]]
break
if segment[0] > position:
next_segment = segment
break
time_to_next = next_segment[0] - position
await skip(time_to_next, next_segment[1], rc)
async def skip(time_to, position, rc):
await asyncio.sleep(time_to)
await rc.set_position(position)
async def connect_atv(loop, identifier, airplay_credentials):
"""Find a device and print what is playing."""
print("Discovering devices on network...")
atvs = await pyatv.scan(loop, identifier = identifier)
if not atvs:
print("No device found, will retry")
return
config = atvs[0]
config.set_credentials(pyatv.Protocol.AirPlay, airplay_credentials)
print(f"Connecting to {config.address}")
return await pyatv.connect(config, loop)
async def loop_atv(event_loop, atv_config, apikey, categories, web_session):
identifier = atv_config["identifier"]
airplay_credentials = atv_config["airplay_credentials"]
atv = await connect_atv(event_loop, identifier, airplay_credentials)
if atv:
listener = MyPushListener(apikey, atv, categories, web_session)
atv.push_updater.listener = listener
atv.push_updater.start()
print("Push updater started")
while True:
await asyncio.sleep(20)
try:
atv.metadata.app
except:
print("Reconnecting to Apple TV")
#reconnect to apple tv
atv = await connect_atv(event_loop, identifier, airplay_credentials)
if atv:
listener = MyPushListener(apikey, atv, categories, web_session)
atv.push_updater.listener = listener
atv.push_updater.start()
print("Push updater started")
def main(atv_configs, apikey, categories, debug):
loop = asyncio.get_event_loop_policy().get_event_loop()
if debug:
loop.set_debug(True)
asyncio.set_event_loop(loop)
web_session = aiohttp.ClientSession()
for i in atv_configs:
loop.create_task(loop_atv(loop, i, apikey, categories, web_session))
loop.run_forever()
if __name__ == "__main__":
print("starting")
main()

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)

33
pyproject.toml Normal file
View File

@@ -0,0 +1,33 @@
[project]
name = "iSponsorBlockTV"
version = "2.6.1"
authors = [
{"name" = "dmunozv04"}
]
description = "SponsorBlock client for all YouTube TV clients"
readme = "README.md"
requires-python = ">=3.9"
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"
[tool.ruff]
line-length = 100

View File

@@ -1,5 +1,10 @@
pyatv
aiohttp
aiodns
async-cache
argparse
aiohttp==3.13.1
appdirs==1.4.4
async-cache==1.1.1
pyytlounge==2.3.0
rich==14.1.0
ssdp==1.3.1
textual==5.3.0
textual-slider==0.2.0
xmltodict==0.15.1
rich_click==1.8.9

View File

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

View File

@@ -0,0 +1,209 @@
import html
from hashlib import sha256
from aiohttp import ClientSession
from cache import AsyncLRU
from . import constants, dial_client
from .conditional_ttl_cache import AsyncConditionalTTL
def list_to_tuple(function):
def wrapper(*args):
args = [tuple(x) if isinstance(x, list) else x for x in args]
result = function(*args)
result = tuple(result) if isinstance(result, list) else result
return result
return wrapper
# Class that handles all the api calls and their cache
class ApiHelper:
def __init__(self, config, web_session: ClientSession) -> None:
self.apikey = config.apikey
self.skip_categories = config.skip_categories
self.channel_whitelist = config.channel_whitelist
self.skip_count_tracking = config.skip_count_tracking
self.web_session = web_session
self.num_devices = len(config.devices)
self.minimum_skip_length = config.minimum_skip_length
# Not used anymore, maybe it can stay here a little longer
@AsyncLRU(maxsize=10)
async def get_vid_id(self, title, artist, api_key, web_session):
params = {"q": title + " " + artist, "key": api_key, "part": "snippet"}
url = constants.Youtube_api + "search"
async with web_session.get(url, params=params) as resp:
data = await resp.json()
if "error" in data:
return
for i in data["items"]:
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
@AsyncLRU(maxsize=100)
async def is_whitelisted(self, vid_id):
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:
if i["id"] == channel_id:
return True
return False
async def __get_channel_id(self, vid_id):
params = {"id": vid_id, "key": self.apikey, "part": "snippet"}
url = constants.Youtube_api + "videos"
async with self.web_session.get(url, params=params) as resp:
data = await resp.json()
if "error" in data:
return
data = data["items"][0]
if data["kind"] != "youtube#video":
return
return data["snippet"]["channelId"]
@AsyncLRU(maxsize=10)
async def search_channels(self, channel):
channels = []
params = {
"q": channel,
"key": self.apikey,
"part": "snippet",
"type": "channel",
"maxResults": "5",
}
url = constants.Youtube_api + "search"
async with self.web_session.get(url, params=params) as resp:
data = await resp.json()
if "error" in data:
return channels
for i in data["items"]:
# Get channel subscription number
params = {
"id": i["snippet"]["channelId"],
"key": self.apikey,
"part": "statistics",
}
url = constants.Youtube_api + "channels"
async with self.web_session.get(url, params=params) as resp:
channel_data = await resp.json()
if channel_data["items"][0]["statistics"]["hiddenSubscriberCount"]:
sub_count = "Hidden"
else:
sub_count = int(channel_data["items"][0]["statistics"]["subscriberCount"])
sub_count = format(sub_count, "_")
channels.append((i["snippet"]["channelId"], i["snippet"]["channelTitle"], sub_count))
return channels
@list_to_tuple # Convert list to tuple so it can be used as a key in the cache
@AsyncConditionalTTL(time_to_live=300, maxsize=10) # 5 minutes for non-locked segments
async def get_segments(self, vid_id):
if await self.is_whitelisted(vid_id):
return (
[],
True,
) # Return empty list and True to indicate
# that the cache should last forever
vid_id_hashed = sha256(vid_id.encode("utf-8")).hexdigest()[
:4
] # Hashes video id and gets the first 4 characters
params = {
"category": self.skip_categories,
"actionType": constants.SponsorBlock_actiontype,
"service": constants.SponsorBlock_service,
}
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_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_json = i
break
return self.process_segments(response_json, self.minimum_skip_length)
@staticmethod
def process_segments(response, minimum_skip_length):
segments = []
ignore_ttl = True
try:
response_segments = response["segments"]
# sort by end
response_segments.sort(key=lambda x: x["segment"][1])
# extend ends of overlapping segments to make one big segment
for i in response_segments:
for j in response_segments:
if j["segment"][0] <= i["segment"][1] <= j["segment"][1]:
i["segment"][1] = j["segment"][1]
# sort by start
response_segments.sort(key=lambda x: x["segment"][0])
# extend starts of overlapping segments to make one big segment
for i in reversed(response_segments):
for j in reversed(response_segments):
if j["segment"][0] <= i["segment"][0] <= j["segment"][1]:
i["segment"][0] = j["segment"][0]
for i in response_segments:
ignore_ttl = (
ignore_ttl and i["locked"] == 1
) # If all segments are locked, ignore ttl
segment = i["segment"]
UUID = i["UUID"]
segment_dict = {"start": segment[0], "end": segment[1], "UUID": [UUID]}
try:
# Get segment before to check if they are too close to each other
segment_before_end = segments[-1]["end"]
segment_before_start = segments[-1]["start"]
segment_before_UUID = segments[-1]["UUID"]
except IndexError:
segment_before_end = -10
if (
segment_dict["start"] - segment_before_end < 1
): # Less than 1 second apart, combine them and skip them together
segment_dict["start"] = segment_before_start
segment_dict["UUID"].extend(segment_before_UUID)
segments.pop()
# Only add segments greater than minimum skip length
if segment_dict["end"] - segment_dict["start"] > minimum_skip_length:
segments.append(segment_dict)
except BaseException:
pass
return segments, ignore_ttl
async def mark_viewed_segments(self, uuids):
"""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 uuids:
url = constants.SponsorBlock_api + "viewedVideoSponsorTime/"
params = {"UUID": i}
await self.web_session.post(url, params=params)
async def discover_youtube_devices_dial(self):
"""Discovers YouTube devices using DIAL"""
dial_screens = await dial_client.discover(self.web_session)
# print(dial_screens)
return dial_screens

View File

@@ -0,0 +1,85 @@
import datetime
from cache.key import KEY
from cache.lru import LRU
# MIT License
# Copyright (c) 2020 Rajat Singh
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# Modified code from https://github.com/iamsinghrajat/async-cache
class AsyncConditionalTTL:
class _TTL(LRU):
def __init__(self, time_to_live, maxsize):
super().__init__(maxsize=maxsize)
self.time_to_live = datetime.timedelta(seconds=time_to_live) if time_to_live else None
self.maxsize = maxsize
def __contains__(self, key):
if key not in self.keys():
return False
key_expiration = super().__getitem__(key)[1]
if key_expiration and key_expiration < datetime.datetime.now():
del self[key]
return False
return True
def __getitem__(self, key):
value = super().__getitem__(key)[0]
return value
def __setitem__(self, key, value):
value, ignore_ttl = value # unpack tuple
ttl_value = (
(datetime.datetime.now() + self.time_to_live)
if (self.time_to_live and not ignore_ttl)
else None
) # ignore ttl if ignore_ttl is True
super().__setitem__(key, (value, ttl_value))
def __init__(self, time_to_live=60, maxsize=1024, skip_args: int = 0):
"""
:param time_to_live: Use time_to_live as None for non expiring cache
:param maxsize: Use maxsize as None for unlimited size cache
:param skip_args: Use `1` to skip first arg of func in determining cache key
"""
self.ttl = self._TTL(time_to_live=time_to_live, maxsize=maxsize)
self.skip_args = skip_args
def __call__(self, func):
async def wrapper(*args, **kwargs):
key = KEY(args[self.skip_args :], kwargs)
if key in self.ttl:
val = self.ttl[key]
else:
self.ttl[key] = await func(*args, **kwargs)
val = self.ttl[key]
return val
wrapper.__name__ += func.__name__
return wrapper

View File

@@ -0,0 +1,212 @@
import asyncio
import aiohttp
from . import api_helpers, ytlounge
# Constants for user input prompts
USE_PROXY_PROMPT = "Do you want to use system-wide proxy? (y/N)"
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: "
MINIMUM_SKIP_PROMPT = "Do you want to specify a minimum length of segment to skip? (y/N)"
MINIMUM_SKIP_SPECIFICATION_PROMPT = (
"Enter minimum length of segment to skip in seconds (enter 0 to disable):"
)
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'.")
return None
async def create_web_session(use_proxy):
return aiohttp.ClientSession(trust_env=use_proxy)
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")
choice = get_yn_input(USE_PROXY_PROMPT)
config.use_proxy = choice == "y"
loop = asyncio.get_event_loop_policy().get_event_loop()
web_session = loop.run_until_complete(create_web_session(config.use_proxy))
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
# Ask for minimum skip length. Confirm input is an integer
minimum_skip_length = config.minimum_skip_length
choice = get_yn_input(MINIMUM_SKIP_PROMPT)
if choice == "y":
while True:
try:
minimum_skip_length = int(input(MINIMUM_SKIP_SPECIFICATION_PROMPT))
break
except ValueError:
print("You entered a non integer value, try again.")
continue
config.minimum_skip_length = minimum_skip_length
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())

View File

@@ -0,0 +1,26 @@
userAgent = "iSponsorBlockTV/0.1"
SponsorBlock_service = "youtube"
SponsorBlock_actiontype = "skip"
SponsorBlock_api = "https://sponsor.ajay.app/api/"
Youtube_api = "https://www.googleapis.com/youtube/v3/"
skip_categories = (
("Sponsor", "sponsor"),
("Self Promotion", "selfpromo"),
("Intro", "intro"),
("Outro", "outro"),
("Music Offtopic", "music_offtopic"),
("Interaction", "interaction"),
("Exclusive Access", "exclusive_access"),
("POI Highlight", "poi_highlight"),
("Preview", "preview"),
("Filler", "filler"),
)
youtube_client_blacklist = ["TVHTML5_FOR_KIDS"]
config_file_blacklist_keys = ["config_file", "data_dir"]
github_wiki_base_url = "https://github.com/dmunozv04/iSponsorBlockTV/wiki"

View File

@@ -0,0 +1,25 @@
class AiohttpTracer:
def __init__(self, logger):
self.logger = logger
async def on_request_start(self, session, context, params):
self.logger.debug(f"Request started ({id(context):#x}): {params.method} {params.url}")
async def on_request_end(self, session, context, params):
self.logger.debug(f"Request ended ({id(context):#x}): {params.response.status}")
async def on_request_exception(self, session, context, params):
self.logger.debug(f"Request exception ({id(context):#x}): {params.exception}")
async def on_response_chunk_received(self, session, context, params):
chunk_size = len(params.chunk)
try:
# Try to decode as text
text = params.chunk.decode("utf-8")
self.logger.debug(f"Response chunk ({id(context):#x}) {chunk_size} bytes: {text}")
except UnicodeDecodeError:
# If not valid UTF-8, show as hex
hex_data = params.chunk.hex()
self.logger.debug(
f"Response chunk ({id(context):#x}) ({chunk_size} bytes) [HEX]: {hex_data}"
)

View File

@@ -0,0 +1,150 @@
"""Send out an M-SEARCH request and listening for responses."""
import asyncio
import socket
import ssdp
import xmltodict
from ssdp import network
# 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
# Copyright (c) 2018 Johannes Hoppe
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# 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)
try:
# doesn't even have to be reachable
s.connect(("10.254.254.254", 1))
ip = s.getsockname()[0]
except Exception:
ip = "127.0.0.1"
finally:
s.close()
return ip
class Handler(ssdp.aio.SSDP):
def __init__(self):
super().__init__()
self.devices = []
def clear(self):
self.devices = []
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}
# print(headers)
if "location" in headers:
self.devices.append(headers["location"])
def request_received(self, request: ssdp.messages.SSDPRequest, addr):
raise NotImplementedError("Request received is not implemented, this is a client")
async def find_youtube_app(web_session, url_location):
async with web_session.get(url_location) as response:
headers = response.headers
response = await response.text()
# print(headers)
data = xmltodict.parse(response)
name = data["root"]["device"]["friendlyName"]
handler = Handler()
handler.clear()
app_url = headers["application-url"]
youtube_url = app_url + "YouTube"
# print(youtube_url)
async with web_session.get(youtube_url) as response:
status_code = response.status
response = await response.text()
# print(status_code)
if status_code == 200:
data = xmltodict.parse(response)
data = data["service"]
screen_id = data["additionalData"]["screenId"]
return {"screen_id": screen_id, "name": name, "offset": 0}
async def discover(web_session):
bind = None
search_target = "urn:dial-multiscreen-org:service:dial:1"
max_wait = 10
handler = Handler()
# Send out an M-SEARCH request and listening for responses
family, _ = network.get_best_family(bind, network.PORT)
loop = asyncio.get_event_loop()
ip_address = get_ip()
connect = loop.create_datagram_endpoint(handler, family=family, local_addr=(ip_address, None))
transport, _ = await connect
target = network.MULTICAST_ADDRESS_IPV4, network.PORT
search_request = ssdp.messages.SSDPRequest(
"M-SEARCH",
headers={
"HOST": f"{target[0]}:{target[1]}",
"MAN": '"ssdp:discover"',
"MX": str(max_wait), # seconds to delay response [1..5]
"ST": search_target,
},
)
target = network.MULTICAST_ADDRESS_IPV4, network.PORT
search_request.sendto(transport, target)
try:
await asyncio.sleep(4)
finally:
transport.close()
devices = []
for i in handler.devices:
devices.append(await find_youtube_app(web_session, i))
return devices

View File

@@ -0,0 +1,217 @@
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
from .constants import config_file_blacklist_keys, github_wiki_base_url
class Device:
def __init__(self, args_dict):
self.screen_id = ""
self.offset = 0
self.__load(args_dict)
self.__validate()
def __load(self, args_dict):
for i in args_dict:
setattr(self, i, args_dict[i])
# Change offset to seconds (from milliseconds)
self.offset = self.offset / 1000
def __validate(self):
if not self.screen_id:
raise ValueError("No screen id found")
class Config:
def __init__(self, data_dir):
self.data_dir = data_dir
self.config_file = data_dir + "/config.json"
self.devices = []
self.apikey = ""
self.skip_categories = [] # These are the categories on the config file
self.channel_whitelist = []
self.skip_count_tracking = True
self.mute_ads = False
self.skip_ads = False
self.minimum_skip_length = 1
self.auto_play = True
self.join_name = "iSponsorBlockTV"
self.use_proxy = False
self.__load()
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:\n"
f"{github_wiki_base_url}/Migrate-from-V1-to-V2"
),
)
print("Exiting in 10 seconds...")
time.sleep(10)
sys.exit()
if not self.devices:
print("No devices found, please add at least one device")
print("Exiting in 10 seconds...")
time.sleep(10)
sys.exit()
self.devices = [Device(i) for i in self.devices]
if not self.apikey and self.channel_whitelist:
raise ValueError("No youtube API key found and channel whitelist is not empty")
if not self.skip_categories:
self.skip_categories = ["sponsor"]
print("No categories found, using default: sponsor")
def __load(self):
try:
with open(self.config_file, "r", encoding="utf-8") as f:
config = json.load(f)
for i in config:
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)
if not os.path.exists(self.data_dir):
if not os.getenv("iSPBTV_docker"):
print("Creating data directory")
os.makedirs(self.data_dir)
else: # Running in docker without mounting the data dir
print(
"Running in docker without mounting the data dir, check the"
" wiki for more information: "
f"{github_wiki_base_url}/Installation#Docker"
)
print(
("This image has recently been updated to v2, and requires changes."),
("Please read this for more information on how to upgrade to V2:"),
f"{github_wiki_base_url}/Migrate-from-V1-to-V2",
)
print("Exiting in 10 seconds...")
time.sleep(10)
sys.exit()
else:
print("Blank config file created")
def save(self):
with open(self.config_file, "w", encoding="utf-8") as f:
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):
return self.__dict__ == other.__dict__
return False
def __hash__(self):
return hash(tuple(sorted(self.items())))
@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")
@click.option("--http-tracing", is_flag=True, help="Enable HTTP request/response tracing")
# 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, http_tracing, setup, setup_cli):
"""iSponsorblockTV"""
ctx.ensure_object(dict)
ctx.obj["data_dir"] = data
ctx.obj["debug"] = debug
ctx.obj["http_tracing"] = http_tracing
logger = logging.getLogger()
ctx.obj["logger"] = logger
sh = logging.StreamHandler()
sh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
logger.addHandler(sh)
if debug:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
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(name="setup")
@click.pass_context
def setup_command(ctx):
"""Setup the program graphically"""
config = Config(ctx.obj["data_dir"])
setup_wizard.main(config)
sys.exit()
@cli.command(name="setup-cli")
@click.pass_context
def setup_cli_command(ctx):
"""Setup the program in the command line"""
config = Config(ctx.obj["data_dir"])
config_setup.main(config, ctx.obj["debug"])
@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"], ctx.obj["http_tracing"])
# 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={})

205
src/iSponsorBlockTV/main.py Normal file
View File

@@ -0,0 +1,205 @@
import asyncio
import logging
import time
from signal import SIGINT, SIGTERM, signal
from typing import Optional
import aiohttp
from . import api_helpers, ytlounge
from .debug_helpers import AiohttpTracer
class DeviceListener:
def __init__(self, api_helper, config, device, debug: bool, web_session):
self.task: Optional[asyncio.Task] = None
self.api_helper = api_helper
self.offset = device.offset
self.name = device.name
self.cancelled = False
self.logger = logging.getLogger(f"iSponsorBlockTV-{device.screen_id}")
self.web_session = web_session
self.lounge_controller = ytlounge.YtLoungeApi(
device.screen_id, config, api_helper, self.logger
)
# Ensures that we have a valid auth token
async def refresh_auth_loop(self):
while True:
await asyncio.sleep(60 * 60 * 24) # Refresh every 24 hours
try:
await self.lounge_controller.refresh_auth()
except BaseException:
pass
async def is_available(self):
try:
return await self.lounge_controller.is_available()
except BaseException:
return False
# Main subscription loop
async def loop(self):
lounge_controller = self.lounge_controller
while not self.cancelled:
while not lounge_controller.linked():
try:
self.logger.debug("Refreshing auth")
await lounge_controller.refresh_auth()
except BaseException:
await asyncio.sleep(10)
while not (await self.is_available()) and not self.cancelled:
self.logger.debug("Waiting for device to be available")
await asyncio.sleep(10)
try:
await lounge_controller.connect()
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)
self.logger.debug("Waiting for device to be connected")
await asyncio.sleep(10)
try:
await lounge_controller.connect()
except BaseException:
pass
self.logger.info(
"Connected to device %s (%s)", lounge_controller.screen_name, self.name
)
try:
self.logger.debug("Subscribing to lounge")
sub = await lounge_controller.subscribe_monitored(self)
await sub
except BaseException:
pass
# Method called on playback state change
async def __call__(self, state):
time_start = time.monotonic()
try:
self.task.cancel()
except BaseException:
pass
self.task = asyncio.create_task(self.process_playstatus(state, time_start))
# Processes the playback state change
async def process_playstatus(self, state, time_start):
segments = []
if state.videoId:
segments = await self.api_helper.get_segments(state.videoId)
if state.state.value == 1: # Playing
self.logger.info("Playing video %s with %d segments", state.videoId, len(segments))
if segments: # If there are segments
await self.time_to_segment(segments, state.currentTime, time_start)
# Finds the next segment to skip to and skips to it
async def time_to_segment(self, segments, position, time_start):
start_next_segment = None
next_segment = None
for segment in segments:
segment_start = segment["start"]
segment_end = segment["end"]
is_within_start_range = (
position < 1 < segment_end and segment_start <= position < segment_end
)
is_beyond_current_position = segment_start > position
if is_within_start_range or is_beyond_current_position:
next_segment = segment
start_next_segment = position if is_within_start_range else segment_start
break
if start_next_segment:
time_to_next = (
(start_next_segment - position - (time.monotonic() - time_start))
/ self.lounge_controller.playback_speed
) - self.offset
await self.skip(time_to_next, next_segment["end"], next_segment["UUID"])
# Skips to the next segment (waits for the time to pass)
async def skip(self, time_to, position, uuids):
await asyncio.sleep(time_to)
self.logger.info("Skipping segment: seeking to %s", position)
await asyncio.gather(
asyncio.create_task(self.lounge_controller.seek_to(position)),
asyncio.create_task(self.api_helper.mark_viewed_segments(uuids)),
)
async def cancel(self):
self.cancelled = True
await self.lounge_controller.disconnect()
if self.task:
self.task.cancel()
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, 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 handle_signal(signum, frame):
raise KeyboardInterrupt()
async def main_async(config, debug, http_tracing):
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)
tcp_connector = aiohttp.TCPConnector(ttl_dns_cache=300)
# Configure session with tracing if enabled
if http_tracing:
root_logger = logging.getLogger("aiohttp_trace")
tracer = AiohttpTracer(root_logger)
trace_config = aiohttp.TraceConfig()
trace_config.on_request_start.append(tracer.on_request_start)
trace_config.on_response_chunk_received.append(tracer.on_response_chunk_received)
trace_config.on_request_end.append(tracer.on_request_end)
trace_config.on_request_exception.append(tracer.on_request_exception)
web_session = aiohttp.ClientSession(
trust_env=config.use_proxy, connector=tcp_connector, trace_configs=[trace_config]
)
else:
web_session = aiohttp.ClientSession(trust_env=config.use_proxy, 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(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()
print("Exited")
def main(config, debug, http_tracing):
asyncio.run(main_async(config, debug, http_tracing))

View File

@@ -0,0 +1,391 @@
.container {
background: $boost;
margin: 1;
padding: 1 1 0 1;
height: auto;
width: 100%;
}
.title {
text-style: bold underline;
dock: top;
background: $primary-background;
width: 100%;
}
.subtitle{
width: 100%;
}
#setup-wizard{
scrollbar-gutter: stable;
}
.button-small {
height: 3;
border: none;
border-top: none;
border-bottom: none;
offset: 0 -1;
padding: 0;
}
.button-100 {
width: 100%;
}
/* Exit screen */
ExitScreen {
align: center middle;
}
#dialog-exit {
grid-size: 3;
grid-gutter: 1 2;
grid-rows: 1fr 3;
padding: 1 2;
width: 35%;
min-width: 50;
max-width: 70;
height: 11;
border: thick $background 80%;
background: $surface;
}
#question {
column-span: 3;
height: 1fr;
width: 1fr;
content-align: center middle;
}
/* Device editor */
EditDevice {
align: center middle;
}
#edit-device-container {
padding: 1 2 0 2;
background: $surface;
border: thick $background 80%;
height: 17;
width: 50%;
min-width: 40;
}
#device-id-container{
grid-size: 4;
grid-gutter: 1 2;
grid-rows: 1fr 3;
width: 100%;
margin-right: 1;
}
#device-id-input{
column-span: 3;
height: 1fr;
width: 1fr;
content-align: center middle;
}
#device-id-view{
width: 1fr;
}
#device-offset-container{
width: 100%;
}
#device-offset-input{
width: 12;
}
#device-offset-slider{
width: 100%;
margin-right: 12;
}
/* devices */
#devices-manager {
min-height: 4;
height: auto;
max-height: 70%;
width: 100%;
scrollbar-gutter: stable;
overflow-y: auto;
}
Element {
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;
padding: 0;
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;
padding: 0;
align: left middle;
text-align: center;
width: 8;
min-width: 8;
margin: 0 1 0 0;
}
#add-device, #add-channel {
text-style: bold;
width: 100%;
align: left middle;
text-align: center;
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;
width: 100%;
margin: 1 1 0 1;
}
/* Add devices */
#add-device-container{
border: thick $background 80%;
background: $surface;
padding: 1 2 0 2;
height: auto;
width: 50%;
min-width: 40;
}
#add-device-switch-buttons{
grid-size: 2 1;
height: 5;
width: 100%;
padding: 1 1;
}
#add-device-pin-container{
height: auto;
}
#add-device-dial-container{
height: auto;
}
#pairing-code-input.-valid {
border: tall $success 60%;
}
#pairing-code-input.-valid:focus {
border: tall $success;
}
#add-device-pin-add-button{
margin: 1 1 0 1;
width: 100%;
}
#add-device-dial-add-button{
margin: 1 1 0 1;
width: 100%;
}
#add-device-info{
height: auto;
min-height: 0;
width: 100%;
padding: 0 1;
}
/* ApiKey */
#api-key-grid{
grid-size: 4;
grid-gutter: 1 2;
grid-rows: 1fr 3;
padding: 1 1;
width: 100%;
height: 5;
}
#api-key-input{
column-span: 3;
height: 1fr;
width: 1fr;
content-align: center middle;
}
/* Skip Categories */
#skip-categories-manager{
min-height: 10;
height: auto;
max-height: 70%;
width: 100%;
scrollbar-gutter: stable;
}
#skip-categories-selection-list{
height: auto;
width: 100%;
background: $boost;
margin: 1;
}
/* Segment count tracking */
#skip-count-tracking-switch{
margin: 1;
}
/* Channel Whitelist */
#channel-whitelist-manager {
min-height: 5;
height: auto;
max-height: 70%;
width: 100%;
scrollbar-gutter: stable;
overflow-y: auto;
}
#add-channel {
text-style: bold;
width: 100%;
align: left middle;
text-align: center;
dock: left;
text-align: left;
}
#add-channel-button-container{
height: 1;
width: 100%;
margin: 1 1 0 1;
}
/* Add Channel */
AddChannel{
align: center middle;
overflow-y: auto;
background: $background 60%;
}
#add-channel-container{
padding: 1 2 0 2;
border: thick $background 80%;
background: $surface;
height: auto;
width: 50%;
min-width: 40;
}
#add-channel-switch-buttons{
grid-size: 2 1;
height: 5;
width: 100%;
padding: 1 1;
}
.button-switcher{
width: 100%;
text-align: center;
}
#add-channel-switcher{
height: auto;
width: auto;
}
#add-channel-switcher-container{
height: auto;
width: 100%;
}
#add-channel-search-container{
height: auto;
}
#add-channel-search-inputs{
height: 3;
width: 100%;
grid-size: 4 1;
margin: 0 1 0 0;
/* padding: 0 1; */
}
#channel-name-input-search{
width: 100%;
height: auto;
column-span: 3;
}
#search-channel-button{
width: 1fr;
height: auto;
column-span: 1;
margin: 0 0 0 0;
}
#channel-search-results{
height: auto;
width: 100%;
background: $boost;
margin: 1;
}
#add-channel-info{
height: auto;
width: 100%;
margin: 0 1;
}
#add-channel-id-container{
height: auto;
}
#channel-name-input-id{
margin: 1 0;
}
#add-channel-search-no-key{
padding: 0 0 1 1;
}
/* Mute/Skip ads */
#ad-skip-mute-container{
padding: 1;
height: auto;
}
/* Migrate screen */
MigrationScreen {
align: center middle;
}
#dialog-migration {
grid-size: 2;
grid-gutter: 1 2;
grid-rows: 1fr 3;
padding: 1 2;
width: 35%;
min-width: 50;
max-width: 70;
height: 11;
border: thick $background 80%;
background: $surface;
}
#question-migrate {
column-span: 2;
height: 1fr;
width: 1fr;
content-align: center middle;
}
/* Autoplay */
#autoplay-container{
padding: 1;
height: auto;
}
/* Use Proxy */
#useproxy-container{
padding: 1;
height: auto;
}

View File

@@ -0,0 +1,998 @@
import asyncio
import copy
import aiohttp
# Textual imports (Textual is awesome!)
from textual import on
from textual.app import App, ComposeResult
from textual.containers import (
Container,
Grid,
Horizontal,
ScrollableContainer,
Vertical,
)
from textual.css.query import NoMatches
from textual.events import Click
from textual.screen import Screen
from textual.validation import Function
from textual.widgets import (
Button,
Checkbox,
ContentSwitcher,
Footer,
Header,
Input,
Label,
RadioButton,
RadioSet,
SelectionList,
Static,
)
from textual.widgets.selection_list import Selection
from textual_slider import Slider
# Local imports
from . import api_helpers, ytlounge
from .constants import skip_categories
def _validate_pairing_code(pairing_code: str) -> bool:
try:
pairing_code = pairing_code.replace("-", "").replace(" ", "")
int(pairing_code)
return len(pairing_code) == 12
except ValueError:
return False # not a number
class ModalWithClickExit(Screen):
"""A modal screen that exits when clicked outside its bounds.
https://discord.com/channels/1026214085173461072/1033754296224841768/1136015817356611604
"""
DEFAULT_CSS = """
ModalWithClickExit {
align: center middle;
layout: vertical;
overflow-y: auto;
background: $background 60%;
}
"""
@on(Click)
def close_out_bounds(self, event: Click) -> None:
if self.get_widget_at(event.screen_x, event.screen_y)[0] is self:
self.dismiss()
class Element(Static):
"""Base class for elements (devices and channels).
It has a name and a remove button.
"""
def __init__(self, element: dict, tooltip: str = None, **kwargs) -> None:
super().__init__(**kwargs)
self.element_data = element
self.element_name = ""
self.process_values_from_data()
self.tooltip = tooltip
def process_values_from_data(self):
raise NotImplementedError("Subclasses must implement this method.")
def compose(self) -> ComposeResult:
yield Button(
label=self.element_name,
classes="element-name button-small",
disabled=True,
id="element-name",
)
yield Button(
"Remove",
classes="element-remove button-small",
variant="error",
id="element-remove",
)
def on_mount(self) -> None:
if self.tooltip:
self.query_one(".element-name").tooltip = self.tooltip
self.query_one(".element-name").disabled = False
class Device(Element):
"""A device element."""
def process_values_from_data(self):
if "name" in self.element_data and self.element_data["name"]:
self.element_name = self.element_data["name"]
else:
self.element_name = (
"Unnamed device with id "
f"{self.element_data['screen_id'][:5]}..."
f"{self.element_data['screen_id'][-5:]}"
)
class Channel(Element):
"""A channel element."""
def process_values_from_data(self):
if "name" in self.element_data:
self.element_name = self.element_data["name"]
else:
self.element_name = f"Unnamed channel with id {self.element_data['channel_id']}"
class ChannelRadio(RadioButton):
"""A radio button for a channel."""
def __init__(self, channel: tuple, **kwargs) -> None:
label = f"{channel[1]} - Subs: {channel[2]}"
super().__init__(label=label, **kwargs)
self.channel_data = channel
class MigrationScreen(ModalWithClickExit):
"""Screen with a dialog to remove old ATVS config."""
BINDINGS = [
("escape", "dismiss()", "Cancel"),
("s", "remove_and_save", "Remove and save"),
("q,ctrl+c", "exit", "Exit"),
]
AUTO_FOCUS = "#exit-save"
def compose(self) -> ComposeResult:
yield Grid(
Label(
(
"Welcome to the new configurator! You seem to have the legacy"
" 'atvs' entry on your config file, do you want to remove it?\n(The"
" app won't start with it present)"
),
id="question",
classes="button-100",
),
Button(
"Remove and save",
variant="primary",
id="migrate-remove-save",
classes="button-100",
),
Button(
"Don't remove",
variant="error",
id="migrate-no-change",
classes="button-100",
),
id="dialog-migration",
)
def action_exit(self) -> None:
self.app.exit()
@on(Button.Pressed, "#migrate-no-change")
def action_no_change(self) -> None:
self.app.pop_screen()
@on(Button.Pressed, "#migrate-remove-save")
def action_remove_and_save(self) -> None:
del self.app.config.atvs
self.app.config.save()
self.app.pop_screen()
class ExitScreen(ModalWithClickExit):
"""Screen with a dialog to exit."""
BINDINGS = [
("escape", "dismiss()", "Cancel"),
("s", "save", "Save"),
("q,ctrl+c", "exit", "Exit"),
]
AUTO_FOCUS = "#exit-save"
def compose(self) -> ComposeResult:
yield Grid(
Label(
"Are you sure you want to exit without saving?",
id="question",
classes="button-100",
),
Button("Save", variant="success", id="exit-save", classes="button-100"),
Button("Don't save", variant="error", id="exit-no-save", classes="button-100"),
Button("Cancel", variant="primary", id="exit-cancel", classes="button-100"),
id="dialog-exit",
)
def action_exit(self) -> None:
self.app.exit()
def action_save(self) -> None:
self.app.config.save()
self.app.exit()
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "exit-no-save":
self.app.exit()
elif event.button.id == "exit-save":
self.app.config.save()
self.app.exit()
else:
self.app.pop_screen()
class AddDevice(ModalWithClickExit):
"""Screen with a dialog to add a device, either with a pairing code
or with lan discovery."""
BINDINGS = [("escape", "dismiss({})", "Return")]
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
self.web_session = aiohttp.ClientSession(trust_env=config.use_proxy)
self.api_helper = api_helpers.ApiHelper(config, self.web_session)
self.devices_discovered_dial = []
def compose(self) -> ComposeResult:
with Container(id="add-device-container"):
yield Label("Add Device", classes="title")
with Grid(id="add-device-switch-buttons"):
yield Button(
"Add with pairing code",
id="add-device-pin-button",
classes="button-switcher",
)
yield Button(
"Add with lan discovery",
id="add-device-dial-button",
classes="button-switcher",
)
with ContentSwitcher(id="add-device-switcher", initial="add-device-pin-container"):
with Container(id="add-device-pin-container"):
yield Input(
placeholder=("Pairing Code (found in Settings - Link with TV code)"),
id="pairing-code-input",
validators=[
Function(_validate_pairing_code, "Invalid pairing code format")
],
)
yield Input(
placeholder="Device Name (auto filled if empty/optional)",
id="device-name-input",
)
yield Button(
"Add",
id="add-device-pin-add-button",
variant="success",
disabled=True,
)
yield Label(id="add-device-info")
with Container(id="add-device-dial-container"):
yield Label(
(
"Make sure your device is on the same network as this"
" 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\n[b][u]If it still doesn't work, "
"pair using a pairing code (it's much more reliable)"
),
classes="subtitle",
)
yield SelectionList(
("Searching for devices...", "", False),
id="dial-devices-list",
disabled=True,
)
yield Button(
"Add selected devices",
id="add-device-dial-add-button",
variant="success",
disabled=True,
)
async def on_mount(self) -> None:
self.devices_discovered_dial = []
asyncio.create_task(self.task_discover_devices())
async def task_discover_devices(self):
devices_found = await self.api_helper.discover_youtube_devices_dial()
try:
list_widget: SelectionList = self.query_one("#dial-devices-list")
except NoMatches:
# The widget was not found, probably the screen was dismissed
return
list_widget.clear_options()
if devices_found:
# print(devices_found)
devices_found_parsed = []
for index, i in enumerate(devices_found):
devices_found_parsed.append(Selection(i["name"], index, False))
list_widget.add_options(devices_found_parsed)
self.query_one("#dial-devices-list").disabled = False
self.devices_discovered_dial = devices_found
else:
list_widget.add_option(("No devices found", "", False))
@on(Button.Pressed, "#add-device-switch-buttons > *")
def handle_switch_buttons(self, event: Button.Pressed) -> None:
self.query_one("#add-device-switcher").current = event.button.id.replace(
"-button", "-container"
)
@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
@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",
)
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(" ", "")
) # remove dashes and spaces
device_name = self.query_one("#device-name-input").value
paired = False
try:
paired = await lounge_controller.pair(pairing_code)
except BaseException:
pass
if paired:
device = {
"screen_id": lounge_controller.auth.screen_id,
"name": device_name if device_name else lounge_controller.screen_name,
"offset": 0,
}
self.query_one("#pairing-code-input").value = ""
self.query_one("#device-name-input").value = ""
self.query_one("#add-device-info").update(
f"[#00ff00][b]Successfully added {device['name']}"
)
self.dismiss([device])
else:
self.query_one("#pairing-code-input").value = ""
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")
def handle_add_device_dial(self) -> None:
list_widget: SelectionList = self.query_one("#dial-devices-list")
selected_devices = list_widget.selected
devices = []
for i in selected_devices:
devices.append(self.devices_discovered_dial[i])
self.dismiss(devices)
@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
class AddChannel(ModalWithClickExit):
"""Screen with a dialog to add a channel,
either using search or with a channel id."""
BINDINGS = [("escape", "dismiss(())", "Return")]
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
web_session = aiohttp.ClientSession(trust_env=config.use_proxy)
self.api_helper = api_helpers.ApiHelper(config, web_session)
def compose(self) -> ComposeResult:
with Container(id="add-channel-container"):
yield Label("Add Channel", classes="title")
yield Label(
(
"Select a method to add a channel. Adding via search only works if"
" a YouTube api key has been set"
),
id="add-channel-label",
classes="subtitle",
)
with Grid(id="add-channel-switch-buttons"):
yield Button(
"Add by channel name",
id="add-channel-search-button",
classes="button-switcher",
)
yield Button(
"Add by channel ID",
id="add-channel-id-button",
classes="button-switcher",
)
yield Label(id="add-channel-info", classes="subtitle")
with ContentSwitcher(id="add-channel-switcher", initial="add-channel-search-container"):
with Vertical(id="add-channel-search-container"):
if self.config.apikey:
with Grid(id="add-channel-search-inputs"):
yield Input(
placeholder="Enter channel name",
id="channel-name-input-search",
)
yield Button("Search", id="search-channel-button", variant="success")
yield RadioSet(
RadioButton(label="Search to see results", disabled=True),
id="channel-search-results",
)
yield Button(
"Add",
id="add-channel-button-search",
variant="success",
disabled=True,
classes="button-100",
)
else:
yield Label(
(
"[#ff0000]No api key set, cannot search for channels."
" You can add it the config section below"
),
id="add-channel-search-no-key",
classes="subtitle",
)
with Vertical(id="add-channel-id-container"):
yield Input(
placeholder=("Enter channel ID (example: UCuAXFkgsw1L7xaCfnd5JJOw)"),
id="channel-id-input",
)
yield Input(
placeholder=(
"Enter channel name (only used to display in the config file)"
),
id="channel-name-input-id",
)
yield Button(
"Add",
id="add-channel-button-id",
variant="success",
classes="button-100",
)
@on(RadioSet.Changed, "#channel-search-results")
def handle_radio_set_changed(self, event: RadioSet.Changed) -> None:
self.query_one("#add-channel-button-search").disabled = False
@on(Button.Pressed, "#add-channel-switch-buttons > *")
def handle_switch_buttons(self, event: Button.Pressed) -> None:
self.query_one("#add-channel-switcher").current = event.button.id.replace(
"-button", "-container"
)
@on(Button.Pressed, "#search-channel-button")
@on(Input.Submitted, "#channel-name-input-search")
async def handle_search_channel(self) -> None:
channel_name = self.query_one("#channel-name-input-search").value
if not channel_name:
self.query_one("#add-channel-info").update("[#ff0000]Please enter a channel name")
return
self.query_one("#search-channel-button").disabled = True
self.query_one("#add-channel-info").update("Searching...")
self.query_one("#add-channel-button-search").disabled = True
self.query_one("#channel-search-results").remove_children()
try:
channels_list = await self.api_helper.search_channels(channel_name)
except BaseException:
self.query_one("#add-channel-info").update("[#ff0000]Failed to search for channel")
self.query_one("#search-channel-button").disabled = False
return
for i in channels_list:
self.query_one("#channel-search-results").mount(ChannelRadio(i))
if channels_list:
self.query_one("#search-channel-button").disabled = False
self.query_one("#add-channel-info").update("")
@on(Button.Pressed, "#add-channel-button-search")
def handle_add_channel_search(self) -> None:
channel = self.query_one("#channel-search-results").pressed_button.channel_data
if not channel:
self.query_one("#add-channel-info").update("[#ff0000]Please select a channel")
return
self.query_one("#add-channel-info").update("Adding...")
self.dismiss(channel)
@on(Button.Pressed, "#add-channel-button-id")
@on(Input.Submitted, "#channel-id-input")
@on(Input.Submitted, "#channel-name-input-id")
def handle_add_channel_id(self) -> None:
channel_id = self.query_one("#channel-id-input").value
channel_name = self.query_one("#channel-name-input-id").value
if not channel_id:
self.query_one("#add-channel-info").update("[#ff0000]Please enter a channel ID")
return
if not channel_name:
channel_name = channel_id
channel = (channel_id, channel_name, "hidden")
self.query_one("#add-channel-info").update("Adding...")
self.dismiss(channel)
class EditDevice(ModalWithClickExit):
"""Screen with a dialog to edit a device. Used by the DevicesManager."""
BINDINGS = [("escape", "close_screen_saving", "Return")]
def __init__(self, device: Element, **kwargs) -> None:
super().__init__(**kwargs)
self.device_data = device.element_data
self.device_widget = device
def action_close_screen_saving(self) -> None:
self.dismiss()
def dismiss(self, _=None) -> None:
self.device_data["name"] = self.query_one("#device-name-input").value
self.device_data["screen_id"] = self.query_one("#device-id-input").value
self.device_data["offset"] = int(self.query_one("#device-offset-input").value)
super().dismiss(self.device_widget)
def compose(self) -> ComposeResult:
name = self.device_data.get("name", "")
offset = self.device_data.get("offset", 0)
with Container(id="edit-device-container"):
yield Label("Edit device (ESCAPE to exit)", classes="title")
yield Label("Device name")
yield Input(placeholder="Device name", id="device-name-input", value=name)
yield Label("Device screen id")
with Grid(id="device-id-container"):
yield Input(
placeholder="Device id",
id="device-id-input",
value=self.device_data["screen_id"],
password=True,
)
yield Button("Show id", id="device-id-view")
yield Label("Device offset (in milliseconds)")
with Horizontal(id="device-offset-container"):
yield Input(id="device-offset-input", value=str(offset))
yield Slider(
name="Device offset",
id="device-offset-slider",
min=0,
max=2000,
step=100,
value=offset,
)
def on_slider_changed(self, event: Slider.Changed) -> None:
offset_input = self.query_one("#device-offset-input")
with offset_input.prevent(Input.Changed):
offset_input.value = str(event.slider.value)
def on_input_changed(self, event: Input.Changed):
if event.input.id == "device-offset-input":
value = event.input.value
if value.isdigit():
value = int(value)
slider = self.query_one("#device-offset-slider")
with slider.prevent(Slider.Changed):
self.query_one("#device-offset-slider").value = value
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "device-id-view":
if "Show" in event.button.label:
event.button.label = "Hide id"
self.query_one("#device-id-input").password = False
else:
event.button.label = "Show id"
self.query_one("#device-id-input").password = True
class DevicesManager(Vertical):
"""Manager for devices, allows adding, edit and removing devices."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
self.devices = config.devices
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 button-small")
for device in self.devices:
yield Device(device, tooltip="Click to edit")
def new_devices(self, device_data) -> None:
if device_data:
device_widget = None
for i in device_data:
self.devices.append(i)
device_widget = Device(i, tooltip="Click to edit")
self.mount(device_widget)
device_widget.focus(scroll_visible=True)
@staticmethod
def edit_device(device_widget: Element) -> None:
device_widget.process_values_from_data()
device_widget.query_one("#element-name").label = device_widget.element_name
@on(Button.Pressed, "#element-remove")
def remove_channel(self, event: Button.Pressed):
channel_to_remove: Element = event.button.parent
self.config.devices.remove(channel_to_remove.element_data)
channel_to_remove.remove()
@on(Button.Pressed, "#add-device")
def add_device(self, event: Button.Pressed):
self.app.push_screen(AddDevice(self.config), callback=self.new_devices)
@on(Button.Pressed, "#element-name")
def edit_channel(self, event: Button.Pressed):
channel_to_edit: Element = event.button.parent
self.app.push_screen(EditDevice(channel_to_edit), callback=self.edit_device)
class ApiKeyManager(Vertical):
"""Manager for the YouTube Api Key."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("YouTube Api Key", classes="title")
yield Label(
"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")
@on(Input.Changed, "#api-key-input")
def changed_api_key(self, event: Input.Changed):
self.config.apikey = event.input.value
@on(Button.Pressed, "#api-key-view")
def pressed_api_key_view(self, event: Button.Pressed):
if "Show" in str(event.button.label):
event.button.label = "Hide key"
self.query_one("#api-key-input").password = False
else:
event.button.label = "Show key"
self.query_one("#api-key-input").password = True
class SkipCategoriesManager(Vertical):
"""Manager for skip categories, allows selecting which categories to skip."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Skip Categories", classes="title")
yield Label("Select the categories you want to skip", classes="subtitle")
skip_categories_parsed = []
for i in skip_categories:
name, value = i
if value in self.config.skip_categories:
skip_categories_parsed.append((name, value, True))
else:
skip_categories_parsed.append((name, value, False))
# print(skip_categories_parsed)
yield SelectionList(*skip_categories_parsed, id="skip-categories-compact-list")
@on(SelectionList.SelectedChanged, "#skip-categories-compact-list")
def changed_skip_categories(self, event: SelectionList.SelectedChanged):
self.config.skip_categories = event.selection_list.selected
class MinimumSkipLengthManager(Vertical):
"""Manager for minimum skip length setting."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Minimum Skip Length", classes="title")
yield Label(
(
"Specify the minimum length a segment must meet in order to skip "
"it (in seconds). Default is 1 second; entering 0 will skip all "
"segments."
),
classes="subtitle",
)
yield Input(
placeholder="Minimum skip length (0 to skip all)",
id="minimum-skip-length-input",
value=str(getattr(self.config, "minimum_skip_length", 1)),
validators=[
Function(
lambda user_input: user_input.isdigit(),
"Please enter a valid non-negative number",
)
],
)
@on(Input.Changed, "#minimum-skip-length-input")
def changed_minimum_skip_length(self, event: Input.Changed):
try:
self.config.minimum_skip_length = int(event.input.value)
except ValueError:
self.config.minimum_skip_length = 1
class SkipCountTrackingManager(Vertical):
"""Manager for skip count tracking, allows to enable/disable skip count tracking."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Skip count tracking", classes="title")
yield Label(
(
"This feature tracks which segments you have skipped to let users know"
" how much their submission has helped others and used as a metric"
" along with upvotes to ensure that spam doesn't get into the database."
" The program sends a message to the sponsor block server each time you"
" skip a segment. Hopefully most people don't change this setting so"
" that the view numbers are accurate. :)"
),
classes="subtitle",
id="skip-count-tracking-subtitle",
)
yield Checkbox(
value=self.config.skip_count_tracking,
id="skip-count-tracking-switch",
label="Enable skip count tracking",
)
@on(Checkbox.Changed, "#skip-count-tracking-switch")
def changed_skip_tracking(self, event: Checkbox.Changed):
self.config.skip_count_tracking = event.checkbox.value
class AdSkipMuteManager(Vertical):
"""Manager for ad skip/mute, allows to enable/disable ad skip/mute."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Skip/Mute ads", classes="title")
yield Label(
(
"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.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")
def changed_mute(self, event: Checkbox.Changed):
self.config.mute_ads = event.checkbox.value
@on(Checkbox.Changed, "#skip-ads-switch")
def changed_skip(self, event: Checkbox.Changed):
self.config.skip_ads = event.checkbox.value
class ChannelWhitelistManager(Vertical):
"""Manager for channel whitelist,
allows adding/removing channels from the whitelist."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Channel Whitelist", classes="title")
yield Label(
(
"This feature allows to whitelist channels from being skipped. This"
" feature is automatically disabled when no channels have been"
" specified."
),
classes="subtitle",
id="channel-whitelist-subtitle",
)
yield Label(
("⚠️ [#FF0000]You need to set your YouTube Api Key in order to use this feature"),
id="warning-no-key",
)
with Horizontal(id="add-channel-button-container"):
yield Button("Add Channel", id="add-channel", classes="button-100 button-small")
for channel in self.config.channel_whitelist:
yield Channel(channel)
def on_mount(self) -> None:
self.app.query_one("#warning-no-key").display = (not self.config.apikey) and bool(
self.config.channel_whitelist
)
def new_channel(self, channel: tuple) -> None:
if channel:
channel_dict = {
"id": channel[0],
"name": channel[1],
}
self.config.channel_whitelist.append(channel_dict)
channel_widget = Channel(channel_dict)
self.mount(channel_widget)
channel_widget.focus(scroll_visible=True)
self.app.query_one("#warning-no-key").display = (not self.config.apikey) and bool(
self.config.channel_whitelist
)
@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)
channel_to_remove.remove()
self.app.query_one("#warning-no-key").display = (not self.config.apikey) and bool(
self.config.channel_whitelist
)
@on(Button.Pressed, "#add-channel")
def add_channel(self, event: Button.Pressed):
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 UseProxyManager(Vertical):
"""Manager for proxy use, allows enabling/disabling use of proxy."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Use proxy", classes="title")
yield Label(
"This feature allows application to use system proxy,"
" if it is set in environment variables."
" This parameter will be passed in all [i]aiohttp.ClientSession[/i]"
' calls. For further information, see "[i]trust_env[/i]" section at'
" [link='https://docs.aiohttp.org/en/stable/client_reference.html']"
"aiohttp documentation[/link].",
classes="subtitle",
id="useproxy-subtitle",
)
with Horizontal(id="useproxy-container"):
yield Checkbox(
value=self.config.use_proxy,
id="useproxy-switch",
label="Use proxy",
)
@on(Checkbox.Changed, "#useproxy-switch")
def changed_skip(self, event: Checkbox.Changed):
self.config.use_proxy = event.checkbox.value
class ISponsorBlockTVSetup(App):
TITLE = "iSponsorBlockTV"
SUB_TITLE = "Setup Wizard"
BINDINGS = [("q,ctrl+c", "exit_modal", "Exit"), ("s", "save", "Save")]
AUTO_FOCUS = None
CSS_PATH = ( # tcss is the recommended extension for textual css files
"setup-wizard-style.tcss"
)
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.dark = True
self.config = config
self.initial_config = copy.deepcopy(config)
def compose(self) -> ComposeResult:
yield Header()
yield Footer()
with ScrollableContainer(id="setup-wizard"):
yield DevicesManager(config=self.config, id="devices-manager", classes="container")
yield SkipCategoriesManager(
config=self.config, id="skip-categories-manager", classes="container"
)
yield MinimumSkipLengthManager(
config=self.config,
id="minimum-skip-length-manager",
classes="container",
)
yield SkipCountTrackingManager(
config=self.config, id="count-segments-manager", classes="container"
)
yield AdSkipMuteManager(
config=self.config, id="ad-skip-mute-manager", classes="container"
)
yield ChannelWhitelistManager(
config=self.config, id="channel-whitelist-manager", classes="container"
)
yield ApiKeyManager(config=self.config, id="api-key-manager", classes="container")
yield AutoPlayManager(config=self.config, id="autoplay-manager", classes="container")
yield UseProxyManager(config=self.config, id="useproxy-manager", classes="container")
def on_mount(self) -> None:
if self.check_for_old_config_entries():
self.app.push_screen(MigrationScreen())
def action_save(self) -> None:
self.config.save()
self.initial_config = copy.deepcopy(self.config)
def action_exit_modal(self) -> None:
if self.config != self.initial_config:
self.app.push_screen(ExitScreen())
else: # No changes were made
self.app.exit()
def check_for_old_config_entries(self) -> bool:
if hasattr(self.config, "atvs"):
return True
return False
@on(Input.Changed, "#api-key-input")
def changed_api_key(self, event: Input.Changed):
try: # ChannelWhitelist might not be mounted
self.app.query_one("#warning-no-key").display = bool(
(not event.input.value) and self.config.channel_whitelist
)
except NoMatches:
pass
def main(config):
app = ISponsorBlockTVSetup(config)
app.run()

View File

@@ -0,0 +1,349 @@
import asyncio
import json
import sys
from typing import Any, List
import pyytlounge
from aiohttp import ClientSession
from pyytlounge.wrapper import NotLinkedException, api_base, as_aiter, Dict
from uuid import uuid4
from .constants import youtube_client_blacklist
create_task = asyncio.create_task
class YtLoungeApi(pyytlounge.YtLoungeApi):
def __init__(
self,
screen_id=None,
config=None,
api_helper=None,
logger=None,
):
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
self.volume_state = {}
self.playback_speed = 1.0
self.subscribe_task = None
self.subscribe_task_watchdog = None
self.callback = None
self.logger = logger
self.shorts_disconnected = False
self.auto_play = True
self.watchdog_running = False
self.last_event_time = 0
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):
"""
Continuous watchdog that monitors for connection health.
If no events are received within the expected timeframe,
it cancels the current subscription.
"""
self.watchdog_running = True
self.last_event_time = asyncio.get_event_loop().time()
try:
while self.watchdog_running:
await asyncio.sleep(10)
current_time = asyncio.get_event_loop().time()
time_since_last_event = current_time - self.last_event_time
# YouTube sends a message at least every 30 seconds
if time_since_last_event > 60:
self.logger.debug(
f"Watchdog triggered: No events for {time_since_last_event:.1f} seconds"
)
# Cancel current subscription
if self.subscribe_task and not self.subscribe_task.done():
self.subscribe_task.cancel()
await asyncio.sleep(1) # Give it time to cancel
except asyncio.CancelledError:
self.logger.debug("Watchdog task cancelled")
self.watchdog_running = False
except BaseException as e:
self.logger.error(f"Watchdog error: {e}")
self.watchdog_running = False
# Subscribe to the lounge and start the watchdog
async def subscribe_monitored(self, callback):
self.callback = callback
# Stop existing watchdog if running
if self.subscribe_task_watchdog and not self.subscribe_task_watchdog.done():
self.watchdog_running = False
self.subscribe_task_watchdog.cancel()
try:
await self.subscribe_task_watchdog
except (asyncio.CancelledError, Exception):
pass
# Start new subscription
if self.subscribe_task and not self.subscribe_task.done():
self.subscribe_task.cancel()
try:
await self.subscribe_task
except (asyncio.CancelledError, Exception):
pass
self.subscribe_task = asyncio.create_task(super().subscribe(callback))
self.subscribe_task_watchdog = asyncio.create_task(self._watchdog())
return self.subscribe_task
# Process a lounge subscription event
# skipcq: PY-R1000
def _process_event(self, event_type: str, args: List[Any]):
self.logger.debug(f"process_event({event_type}, {args})")
# Update last event time for the watchdog
self.last_event_time = asyncio.get_event_loop().time()
# 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]
# print(data)
# Unmute when the video starts playing
if self.mute_ads and data["state"] == "1":
create_task(self.mute(False, override=True))
elif event_type == "nowPlaying":
data = args[0]
# Unmute when the video starts playing
if self.mute_ads and data.get("state", "0") == "1":
self.logger.info("Ad has ended, unmuting")
create_task(self.mute(False, override=True))
elif event_type == "onAdStateChange":
data = args[0]
if data["adState"] == "0" and data["currentTime"] != "0": # Ad is not playing
self.logger.info("Ad has ended, unmuting")
create_task(self.mute(False, override=True))
elif (
self.skip_ads and data["isSkipEnabled"] == "true"
): # YouTube uses strings for booleans
self.logger.info("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
self.logger.info("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)
elif event_type == "onVolumeChanged":
self.volume_state = args[0]
# 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
self.logger.info(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":
data = args[0]
# Gets segments for the next video (after the ad) before it starts playing
if vid_id := data["contentVideoId"]:
self.logger.info(f"Getting segments for next video: {vid_id}")
create_task(self.api_helper.get_segments(vid_id))
if (
self.skip_ads and data["isSkipEnabled"] == "true"
): # YouTube uses strings for booleans
self.logger.info("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
self.logger.info("Ad has started, muting")
create_task(self.mute(True, override=True))
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 == "onSubtitlesTrackChanged":
if self.shorts_disconnected:
data = args[0]
video_id_saved = data.get("videoId", None)
self.shorts_disconnected = False
create_task(self.play_video(video_id_saved))
elif event_type == "loungeScreenDisconnected":
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))
elif event_type == "onPlaybackSpeedChanged":
data = args[0]
self.playback_speed = float(data.get("playbackSpeed", "1"))
create_task(self.get_now_playing())
super()._process_event(event_type, args)
# Set the volume to a specific value (0-100)
async def set_volume(self, volume: int) -> None:
await self._command("setVolume", {"volume": volume})
async def mute(self, mute: bool, override: bool = False) -> None:
"""
Mute or unmute the device (if the device already
is in the desired state, nothing happens)
:param bool mute: True to mute, False to unmute
:param bool override: If True, the command is sent even if the
device already is in the desired state
TODO: Only works if the device is subscribed to the lounge
"""
if mute:
mute_str = "true"
else:
mute_str = "false"
if override or not self.volume_state.get("muted", "false") == mute_str:
self.volume_state["muted"] = mute_str
# YouTube wants the volume when unmuting, so we send it
await self._command(
"setVolume",
{"volume": self.volume_state.get("volume", 100), "muted": mute_str},
)
async def play_video(self, video_id: str) -> bool:
return await self._command("setPlaylist", {"videoId": video_id})
async def get_now_playing(self):
return await self._command("getNowPlaying")
# 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:
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
def _common_connection_parameters(self) -> Dict[str, Any]:
return {
"name": self.device_name,
"loungeIdToken": self.auth.lounge_id_token,
"SID": self._sid,
"AID": self._last_event_id,
"gsessionid": self._gsession,
"device": "REMOTE_CONTROL",
"app": "ytios-phone-20.15.1",
"VER": "8",
"v": "2",
}
async def connect(self) -> bool:
"""Attempt to connect using the previously set tokens"""
if not self.linked():
raise NotLinkedException("Not linked")
connect_body = {
"id": self.auth.screen_id,
"mdx-version": "3",
"TYPE": "xmlhttp",
"theme": "cl",
"sessionSource": "MDX_SESSION_SOURCE_UNKNOWN",
"connectParams": '{"setStatesParams": "{"playbackSpeed":0}"}',
"RID": "1",
"CVER": "1",
"capabilities": "que,dsdtr,atp,vsp",
"ui": "false",
"app": "ytios-phone-20.15.1",
"pairing_type": "manual",
"VER": "8",
"loungeIdToken": self.auth.lounge_id_token,
"device": "REMOTE_CONTROL",
"name": self.device_name,
}
connect_url = f"{api_base}/bc/bind"
async with self.session.post(url=connect_url, data=connect_body) as resp:
try:
text = await resp.text()
if resp.status == 401:
if "Connection denied" in text:
self._logger.warning(
"Connection denied, attempting to circumvent the issue"
)
await self.connect_as_screen()
# self._lounge_token_expired()
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(as_aiter(lines)):
self._process_events(events)
self._command_offset = 1
return self.connected()
except:
self._logger.exception(
"Handle connect failed, status %s reason %s",
resp.status,
resp.reason,
)
raise
async def connect_as_screen(self) -> bool:
"""Attempt to connect using the previously set tokens"""
if not self.linked():
raise NotLinkedException("Not linked")
connect_body = {
"id": str(uuid4()),
"mdx-version": "3",
"TYPE": "xmlhttp",
"theme": "cl",
"sessionSource": "MDX_SESSION_SOURCE_UNKNOWN",
"connectParams": '{"setStatesParams": "{"playbackSpeed":0}"}',
"sessionNonce": str(uuid4()),
"RID": "1",
"CVER": "1",
"capabilities": "que,dsdtr,atp,vsp",
"ui": "false",
"app": "ytios-phone-20.15.1",
"pairing_type": "manual",
"VER": "8",
"loungeIdToken": self.auth.lounge_id_token,
"device": "LOUNGE_SCREEN",
"name": self.device_name,
}
connect_url = f"{api_base}/bc/bind"
async with self.session.post(url=connect_url, data=connect_body) as resp:
try:
await resp.text()
self.logger.error(
"Connected as screen: please force close the app on the device for iSponsorBlockTV to work properly"
)
self.logger.warn("Exiting in 5 seconds")
await asyncio.sleep(5)
sys.exit(0)
except:
self._logger.exception(
"Handle connect failed, status %s reason %s",
resp.status,
resp.reason,
)
raise

View File

@@ -1,3 +1,3 @@
from iSponsorBlockTV import helpers
helpers.app_start()
helpers.app_start()

5
src/main_tui.py Normal file
View File

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