Compare commits

...

239 Commits

Author SHA1 Message Date
dependabot[bot]
72ad37babe Bump python from 3.13-alpine3.21 to 3.14-alpine3.21
Bumps python from 3.13-alpine3.21 to 3.14-alpine3.21.

---
updated-dependencies:
- dependency-name: python
  dependency-version: 3.14-alpine3.21
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 20:13:10 +00: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
boltgolt
4de056d2a8 Allow setting device name to custom string 2023-10-17 17:55:36 +02:00
28 changed files with 805 additions and 528 deletions

View File

@@ -6,3 +6,4 @@ enabled = true
[analyzers.meta]
runtime_version = "3.x.x"
max_line_length = 100

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

@@ -6,7 +6,8 @@ on:
branches:
- '*'
tags:
- 'v*'
- 'v*.*.*'
- 'v*.*.*-*'
pull_request:
branches:
- '*'
@@ -21,22 +22,28 @@ permissions:
jobs:
build:
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
runs-on: ubuntu-latest
steps:
# Get the repository's code
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
# Generate docker tags
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/dmunozv04/isponsorblocktv, dmunozv04/isponsorblocktv
images: |
ghcr.io/${{ github.repository }}
${{ env.DOCKERHUB_USERNAME && 'dmunozv04/isponsorblocktv' || '' }}
tags: |
type=raw,value=develop,priority=900,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
type=ref,enable=true,priority=600,prefix=pr-,suffix=,event=pr
type=ref,event=tag
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}
type=semver,pattern=v{{major}}.{{minor}}
type=ref,event=branch
type=schedule
@@ -49,7 +56,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
if: github.event_name != 'pull_request' && env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -64,13 +71,12 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64, linux/arm64, linux/arm/v7
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=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache
# Only cache if it's not a pull request
cache-to: ${{ github.event_name != 'pull_request' && 'type=registry,ref=ghcr.io/dmunozv04/isponsorblocktv:buildcache,mode=max' || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -11,12 +11,9 @@ name: Release Package
on:
push:
branches:
- '*'
- 'main'
tags:
- 'v*'
pull_request:
branches:
- '*'
release:
types: [published]
@@ -33,10 +30,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -57,7 +54,10 @@ jobs:
build-binaries:
name: Build binaries for ${{ matrix.job.target }} (${{ matrix.job.os }})
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 }}
@@ -70,20 +70,26 @@ jobs:
os: ubuntu-latest
cross: true
release_suffix: x86_64-linux
- target: aarch64-unknown-linux-gnu
- 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-2022
os: windows-latest
release_suffix: x86_64-windows
# macOS
- target: aarch64-apple-darwin
os: macos-14
os: macos-latest
release_suffix: aarch64-osx
- target: x86_64-apple-darwin
os: macos-12
os: macos-latest
cross: true
release_suffix: x86_64-osx
env:
@@ -92,18 +98,19 @@ jobs:
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.23.0
PYAPP_VERSION: v0.28.0
steps:
- name: Checkout
uses: actions/checkout@v4
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@v5
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -134,7 +141,7 @@ jobs:
hatch --version
- name: Get artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: sdist-and-wheel
path: ${{ github.workspace }}/dist
@@ -151,6 +158,12 @@ jobs:
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:
@@ -168,7 +181,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Get artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: sdist-and-wheel
path: dist
@@ -187,7 +200,7 @@ jobs:
if: github.event_name == 'release' && github.event.action == 'published'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
name: Get artifact
with:
path: dist

View File

@@ -18,11 +18,11 @@ jobs:
steps:
# Get the repository's code
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
# Update description
- name: Update repo description
uses: peter-evans/dockerhub-description@v4
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

1
.markdownlintignore Normal file
View File

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

View File

@@ -3,7 +3,7 @@
# Inspired by textual pre-commit config
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: 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
@@ -18,22 +18,14 @@ repos:
- id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline
- id: mixed-line-ending # replaces or checks mixed line ending
- id: trailing-whitespace # checks for trailing whitespace
- repo: https://github.com/pycqa/isort
rev: 5.12.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.12
hooks:
- id: isort
name: isort (python)
language_version: '3.11'
args: ["--profile", "black", "--filter-files"]
- repo: https://github.com/psf/black
rev: 23.1.0
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
- id: ruff-format
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.45.0
hooks:
- id: black
language_version: '3.11'
args: ["--preview"]
- repo: https://github.com/hadialqattan/pycln # removes unused imports
rev: v2.3.0
hooks:
- id: pycln
language_version: "3.11"
args: [--all]
- id: markdownlint
args: ["--fix"]

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM python:3.11-alpine3.19 AS base
FROM python:3.14-alpine3.21 AS base
FROM base AS compiler
@@ -19,9 +19,9 @@ RUN apk add --no-cache gcc musl-dev && \
pip install -r requirements.txt && \
pip uninstall -y pip wheel && \
apk del gcc musl-dev && \
python3 -m compileall -b -f /usr/local/lib/python3.11/site-packages && \
find /usr/local/lib/python3.11/site-packages -name "*.py" -type f -delete && \
find /usr/local/lib/python3.11/ -name "__pycache__" -type d -exec rm -rf {} +
python3 -m compileall -b -f /usr/local/lib/python3.13/site-packages && \
find /usr/local/lib/python3.13/site-packages -name "*.py" -type f -delete && \
find /usr/local/lib/python3.13/ -name "__pycache__" -type d -exec rm -rf {} +
FROM base

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

View File

@@ -1,21 +1,29 @@
# iSponsorBlockTV
Skip sponsor segments in YouTube videos playing on a YouTube TV device (see below for compatibility details).
This project is written in asynchronous python and should be pretty quick.
[![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)
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
Check the [wiki](https://github.com/dmunozv04/iSponsorBlockTV/wiki/Installation)
Warning: docker armv7 builds have been deprecated. Amd64 and arm64 builds are still available.
## Compatibility
Legend: ✅ = Working, ❌ = Not working, ❔ = Not tested
Open an issue/pull request if you have tested a device that isn't listed here.
| Device | Status |
|:-------------------|:------:|
| Apple TV | ✅ |
| Apple TV | ✅* |
| Samsung TV (Tizen) | ✅ |
| LG TV (WebOS) | ✅ |
| Android TV | ✅ |
@@ -28,25 +36,34 @@ Open an issue/pull request if you have tested a device that isn't listed here.
| Xbox One/Series | ✅ |
| Playstation 4/5 | ✅ |
## Usage
Run iSponsorBlockTV on a computer that has network access.
Auto discovery will require the computer to be on the same network as the device during setup.
The device can also be manually added to iSponsorBlockTV with a YouTube TV code. This code can be found in the settings page of your YouTube application.
*Ad muting won't work when using AirPlay to send the audio to another speaker.
It connects to the device, watches its activity and skips any sponsor segment using the [SponsorBlock](https://sponsor.ajay.app/) API.
It can also skip/mute YouTube ads.
## Usage
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.
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.
## Libraries used
- [pyytlounge](https://github.com/FabioGNR/pyytlounge) Used to interact with the device
- [pyytlounge](https://github.com/FabioGNR/pyytlounge) Used to interact with the
device
- asyncio and [aiohttp](https://github.com/aio-libs/aiohttp)
- [async-cache](https://github.com/iamsinghrajat/async-cache)
- [Textual](https://github.com/textualize/textual/) Used for the amazing new graphical configurator
- [Textual](https://github.com/textualize/textual/) Used for the amazing new
graphical configurator
- [ssdp](https://github.com/codingjoe/ssdp) Used for auto discovery
## Projects using this project
- [Home Assistant Addon](https://github.com/bertybuttface/addons/tree/main/isponsorblocktv)
## Contributing
1. Fork it (<https://github.com/dmunozv04/iSponsorBlockTV/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
@@ -54,8 +71,11 @@ It can also skip/mute YouTube ads.
5. Create a new Pull Request
## Contributors
- [dmunozv04](https://github.com/dmunozv04) - creator and maintainer
- [HaltCatchFire](https://github.com/HaltCatchFire) - updated dependencies and improved skip logic
- [Oxixes](https://github.com/oxixes) - added support for channel whitelist and minor improvements
[![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

@@ -12,11 +12,14 @@
"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
}

View File

@@ -1,4 +1,3 @@
version: '3.3'
services:
iSponsorBlockTV:
image: ghcr.io/dmunozv04/isponsorblocktv

View File

@@ -1,12 +1,12 @@
[project]
name = "iSponsorBlockTV"
version = "2.2.1"
version = "2.6.0"
authors = [
{"name" = "dmunozv04"}
]
description = "SponsorBlock client for all YouTube TV clients"
readme = "README.md"
requires-python = ">=3.7"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
@@ -29,5 +29,5 @@ files = ["requirements.txt"]
requires = ["hatchling", "hatch-requirements-txt"]
build-backend = "hatchling.build"
[tool.black]
line-length = 88
[tool.ruff]
line-length = 100

View File

@@ -1,11 +1,10 @@
aiohttp==3.9.5
aiohttp==3.12.15
appdirs==1.4.4
argparse==1.4.0
async-cache==1.1.1
pyytlounge==2.0.0
rich==13.7.1
ssdp==1.3.0
textual==0.58.0
textual-slider==0.1.1
xmltodict==0.13.0
rich_click==1.8.3
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

@@ -27,6 +27,7 @@ class ApiHelper:
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)
@@ -101,26 +102,21 @@ class ApiHelper:
if channel_data["items"][0]["statistics"]["hiddenSubscriberCount"]:
sub_count = "Hidden"
else:
sub_count = int(
channel_data["items"][0]["statistics"]["subscriberCount"]
)
sub_count = int(channel_data["items"][0]["statistics"]["subscriberCount"])
sub_count = format(sub_count, "_")
channels.append(
(i["snippet"]["channelId"], i["snippet"]["channelTitle"], 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
@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
) # 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
@@ -131,9 +127,7 @@ class ApiHelper:
}
headers = {"Accept": "application/json"}
url = constants.SponsorBlock_api + "skipSegments/" + vid_id_hashed
async with self.web_session.get(
url, headers=headers, params=params
) as response:
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()
@@ -146,10 +140,10 @@ class ApiHelper:
if str(i["videoID"]) == str(vid_id):
response_json = i
break
return self.process_segments(response_json)
return self.process_segments(response_json, self.minimum_skip_length)
@staticmethod
def process_segments(response):
def process_segments(response, minimum_skip_length):
segments = []
ignore_ttl = True
try:
@@ -183,7 +177,7 @@ class ApiHelper:
segment_before_start = segments[-1]["start"]
segment_before_UUID = segments[-1]["UUID"]
except Exception:
except IndexError:
segment_before_end = -10
if (
segment_dict["start"] - segment_before_end < 1
@@ -191,13 +185,16 @@ class ApiHelper:
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 Exception:
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.
"""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:

View File

@@ -3,28 +3,29 @@ import datetime
from cache.key import KEY
from cache.lru import LRU
"""MIT License
# MIT License
Copyright (c) 2020 Rajat Singh
# 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:
# 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 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"""
# 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:
@@ -32,9 +33,7 @@ class AsyncConditionalTTL:
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.time_to_live = datetime.timedelta(seconds=time_to_live) if time_to_live else None
self.maxsize = maxsize

View File

@@ -5,16 +5,14 @@ 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) "
"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) "
)
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 = (
@@ -22,13 +20,15 @@ ENTER_SKIP_CATEGORIES_PROMPT = (
" 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) "
)
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) "
@@ -43,11 +43,17 @@ def get_yn_input(prompt):
if choice.lower() in ["y", "n"]:
return choice.lower()
print("Invalid input. Please enter 'y' or 'n'.")
return None
async def pair_device():
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("iSponsorBlockTV")
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(" ", "")
@@ -70,8 +76,12 @@ async def pair_device():
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 = aiohttp.ClientSession()
web_session = loop.run_until_complete(create_web_session(config.use_proxy))
if debug:
loop.set_debug(True)
asyncio.set_event_loop(loop)
@@ -88,9 +98,7 @@ def main(config, debug: bool) -> None:
devices = config.devices
choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices)))
while choice == "y":
task = loop.create_task(pair_device())
loop.run_until_complete(task)
device = task.result()
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)))
@@ -117,15 +125,11 @@ def main(config, debug: bool) -> None:
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
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
skip_categories = [x for x in skip_categories if x != ""] # Remove empty strings
config.skip_categories = skip_categories
channel_whitelist = config.channel_whitelist
@@ -144,9 +148,7 @@ def main(config, debug: bool) -> None:
if channel == "/exit":
break
task = loop.create_task(
api_helper.search_channels(channel, apikey, web_session)
)
task = loop.create_task(api_helper.search_channels(channel, apikey, web_session))
loop.run_until_complete(task)
results = task.result()
if len(results) == 0:
@@ -178,6 +180,21 @@ def main(config, debug: bool) -> None:
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"

View File

@@ -22,3 +22,5 @@ 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

@@ -1,4 +1,5 @@
"""Send out an M-SEARCH request and listening for responses."""
import asyncio
import socket
@@ -6,46 +7,47 @@ 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."""
# 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
# MIT License
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:
# Copyright (c) 2018 Johannes Hoppe
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
# 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 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"""
# 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():
@@ -80,6 +82,9 @@ class Handler(ssdp.aio.SSDP):
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:
@@ -110,21 +115,19 @@ async def discover(web_session):
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, addr = network.get_best_family(bind, network.PORT)
# 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, protocol = await connect
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": "%s:%d" % target,
"HOST": f"{target[0]}:{target[1]}",
"MAN": '"ssdp:discover"',
"MX": str(max_wait), # seconds to delay response [1..5]
"ST": search_target,
@@ -135,7 +138,6 @@ async def discover(web_session):
search_request.sendto(transport, target)
# print(search_request, addr[:2])
try:
await asyncio.sleep(4)
finally:

View File

@@ -8,7 +8,7 @@ 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
from .constants import config_file_blacklist_keys, github_wiki_base_url
class Device:
@@ -41,7 +41,10 @@ class Config:
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):
@@ -50,8 +53,8 @@ class Config:
(
"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"
"on how to upgrade to V2:\n"
f"{github_wiki_base_url}/Migrate-from-V1-to-V2"
),
)
print("Exiting in 10 seconds...")
@@ -64,9 +67,7 @@ class Config:
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"
)
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")
@@ -89,18 +90,12 @@ class Config:
print(
"Running in docker without mounting the data dir, check the"
" wiki for more information: "
"https://github.com/dmunozv04/iSponsorBlockTV/wiki/Installation#Docker"
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:"
),
"https://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2",
("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)
@@ -125,20 +120,21 @@ class 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"),
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", is_flag=True, help="Setup the program graphically", hidden=True)
@click.option(
"--setup-cli",
is_flag=True,
@@ -146,13 +142,24 @@ class Config:
hidden=True,
)
@click.pass_context
def cli(ctx, data, debug, setup, setup_cli):
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:
logging.basicConfig(level=logging.DEBUG)
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
if ctx.invoked_subcommand is None:
if setup:
ctx.invoke(setup_command)
@@ -162,36 +169,30 @@ def cli(ctx, data, debug, setup, setup_cli):
ctx.invoke(start)
@cli.command()
@cli.command(name="setup")
@click.pass_context
def setup(ctx):
def setup_command(ctx):
"""Setup the program graphically"""
config = Config(ctx.obj["data_dir"])
setup_wizard.main(config)
sys.exit()
setup_command = setup
@cli.command()
@cli.command(name="setup-cli")
@click.pass_context
def setup_cli(ctx):
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"])
setup_cli_command = setup_cli
@cli.command()
@click.pass_context
def start(ctx):
"""Start the main program"""
config = Config(ctx.obj["data_dir"])
config.validate()
main.main(config, ctx.obj["debug"])
main.main(config, ctx.obj["debug"], ctx.obj["http_tracing"])
# Create fake "self" group to show pyapp options in help menu
@@ -201,9 +202,7 @@ 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"
)
click.Command("remove", help="Remove the package, wiping the installation but not the data")
)
pyapp_group.add_command(
click.RichCommand(

View File

@@ -1,57 +0,0 @@
import os
import plistlib
from . import config_setup
"""Not updated to V2 yet, should still work. Here be dragons"""
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
+ "opening now on finder..."
)
os.system("open -R " + correct_path)

View File

@@ -7,6 +7,7 @@ from typing import Optional
import aiohttp
from . import api_helpers, ytlounge
from .debug_helpers import AiohttpTracer
class DeviceListener:
@@ -18,18 +19,8 @@ class DeviceListener:
self.cancelled = False
self.logger = logging.getLogger(f"iSponsorBlockTV-{device.screen_id}")
self.web_session = web_session
if debug:
self.logger.setLevel(logging.DEBUG)
else:
self.logger.setLevel(logging.INFO)
sh = logging.StreamHandler()
sh.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
self.logger.addHandler(sh)
self.logger.info(f"Starting device")
self.lounge_controller = ytlounge.YtLoungeApi(
device.screen_id, config, api_helper, self.logger, self.web_session
device.screen_id, config, api_helper, self.logger
)
# Ensures that we have a valid auth token
@@ -38,15 +29,13 @@ class DeviceListener:
await asyncio.sleep(60 * 60 * 24) # Refresh every 24 hours
try:
await self.lounge_controller.refresh_auth()
except:
# traceback.print_exc()
except BaseException:
pass
async def is_available(self):
try:
return await self.lounge_controller.is_available()
except:
# traceback.print_exc()
except BaseException:
return False
# Main subscription loop
@@ -57,38 +46,40 @@ class DeviceListener:
try:
self.logger.debug("Refreshing auth")
await lounge_controller.refresh_auth()
except:
except BaseException:
await asyncio.sleep(10)
while not (await self.is_available()) and not self.cancelled:
self.logger.debug("Waiting for device to be available")
await asyncio.sleep(10)
try:
await lounge_controller.connect()
except:
except BaseException:
pass
while not lounge_controller.connected() and not self.cancelled:
# Doesn't connect to the device if it's a kids profile (it's broken)
self.logger.debug("Waiting for device to be connected")
await asyncio.sleep(10)
try:
await lounge_controller.connect()
except:
except BaseException:
pass
self.logger.info(
"Connected to device %s (%s)", lounge_controller.screen_name, self.name
)
try:
self.logger.info("Subscribing to lounge")
self.logger.debug("Subscribing to lounge")
sub = await lounge_controller.subscribe_monitored(self)
await sub
except:
except BaseException:
pass
# Method called on playback state change
async def __call__(self, state):
time_start = time.monotonic()
try:
self.task.cancel()
except:
except BaseException:
pass
time_start = time.time()
self.task = asyncio.create_task(self.process_playstatus(state, time_start))
# Processes the playback state change
@@ -97,9 +88,7 @@ class DeviceListener:
if state.videoId:
segments = await self.api_helper.get_segments(state.videoId)
if state.state.value == 1: # Playing
self.logger.info(
f"Playing video {state.videoId} with {len(segments)} segments"
)
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)
@@ -108,63 +97,109 @@ class DeviceListener:
start_next_segment = None
next_segment = None
for segment in segments:
if position < 2 and (segment["start"] <= position < segment["end"]):
next_segment = segment
start_next_segment = (
position # different variable so segment doesn't change
segment_start = segment["start"]
segment_end = segment["end"]
is_within_start_range = (
position < 1 < segment_end and segment_start <= position < segment_end
)
break
if segment["start"] > position:
is_beyond_current_position = segment_start > position
if is_within_start_range or is_beyond_current_position:
next_segment = segment
start_next_segment = next_segment["start"]
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.time() - time_start) - self.offset
)
(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.create_task(self.api_helper.mark_viewed_segments(uuids))
await asyncio.create_task(self.lounge_controller.seek_to(position))
await asyncio.gather(
asyncio.create_task(self.lounge_controller.seek_to(position)),
asyncio.create_task(self.api_helper.mark_viewed_segments(uuids)),
)
# Stops the connection to the device
async def cancel(self):
self.cancelled = True
try:
await self.lounge_controller.disconnect()
if self.task:
self.task.cancel()
except Exception:
pass
if self.lounge_controller.subscribe_task_watchdog:
self.lounge_controller.subscribe_task_watchdog.cancel()
if self.lounge_controller.subscribe_task:
self.lounge_controller.subscribe_task.cancel()
await asyncio.gather(
self.task,
self.lounge_controller.subscribe_task_watchdog,
self.lounge_controller.subscribe_task,
return_exceptions=True,
)
async def initialize_web_session(self):
await self.lounge_controller.change_web_session(self.web_session)
async def finish(devices):
for i in devices:
await i.cancel()
async def finish(devices, web_session, tcp_connector):
await asyncio.gather(*(device.cancel() for device in devices), return_exceptions=True)
await web_session.close()
await tcp_connector.close()
def main(config, debug):
def handle_signal(signum, frame):
raise KeyboardInterrupt()
async def main_async(config, debug, 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)
asyncio.set_event_loop(loop)
tcp_connector = aiohttp.TCPConnector(ttl_dns_cache=300)
web_session = aiohttp.ClientSession(loop=loop, connector=tcp_connector)
# 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(SIGINT, lambda s, f: loop.stop())
signal(SIGTERM, lambda s, f: loop.stop())
loop.run_forever()
signal(SIGTERM, handle_signal)
signal(SIGINT, handle_signal)
try:
await asyncio.gather(*tasks)
except KeyboardInterrupt:
print("Cancelling tasks and exiting...")
loop.run_until_complete(finish(devices))
loop.run_until_complete(web_session.close())
loop.run_until_complete(tcp_connector.close())
loop.close()
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

@@ -21,9 +21,13 @@
scrollbar-gutter: stable;
}
.small-button{
.button-small {
height: 3;
border: none;
border-top: none;
border-bottom: none;
offset: 0 -1;
padding: 0;
}
.button-100 {
@@ -106,13 +110,14 @@ EditDevice {
}
Element {
background: $panel;
background: $panel-darken-1;
border-top: solid $panel-lighten-2;
layout: horizontal;
height: 2;
width: 100%;
margin: 0 1 0 1;
padding: 0;
}
Element > .element-name {
offset: 0 -1;
@@ -120,7 +125,11 @@ Element > .element-name {
width: 100%;
align: left middle;
text-align: left;
background: $panel-darken-1;
&:hover {
background: $panel-lighten-1;
border-top: tall $panel-lighten-3;
}
}
Element > .element-remove {
dock: right;
@@ -132,7 +141,7 @@ Element > .element-remove {
margin: 0 1 0 0;
}
#add-device {
#add-device, #add-channel {
text-style: bold;
width: 100%;
align: left middle;
@@ -140,6 +149,11 @@ Element > .element-remove {
dock: left;
text-align: left;
background: $panel-darken-1;
&:hover {
background: $panel-lighten-1;
border-top: tall $panel-lighten-3;
}
}
#add-device-button-container{
height: 1;
@@ -369,3 +383,9 @@ MigrationScreen {
padding: 1;
height: auto;
}
/* Use Proxy */
#useproxy-container{
padding: 1;
height: auto;
}

View File

@@ -13,6 +13,7 @@ from textual.containers import (
ScrollableContainer,
Vertical,
)
from textual.css.query import NoMatches
from textual.events import Click
from textual.screen import Screen
from textual.validation import Function
@@ -79,17 +80,20 @@ class Element(Static):
self.tooltip = tooltip
def process_values_from_data(self):
pass
raise NotImplementedError("Subclasses must implement this method.")
def compose(self) -> ComposeResult:
yield Button(
label=self.element_name,
classes="element-name",
classes="element-name button-small",
disabled=True,
id="element-name",
)
yield Button(
"Remove", classes="element-remove", variant="error", id="element-remove"
"Remove",
classes="element-remove button-small",
variant="error",
id="element-remove",
)
def on_mount(self) -> None:
@@ -102,7 +106,6 @@ class Device(Element):
"""A device element."""
def process_values_from_data(self):
print(self.element_data)
if "name" in self.element_data and self.element_data["name"]:
self.element_name = self.element_data["name"]
else:
@@ -120,9 +123,7 @@ class Channel(Element):
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']}"
)
self.element_name = f"Unnamed channel with id {self.element_data['channel_id']}"
class ChannelRadio(RadioButton):
@@ -202,9 +203,7 @@ class ExitScreen(ModalWithClickExit):
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("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",
)
@@ -227,14 +226,15 @@ class ExitScreen(ModalWithClickExit):
class AddDevice(ModalWithClickExit):
"""Screen with a dialog to add a device, either with a pairing code or with lan discovery."""
"""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()
self.web_session = aiohttp.ClientSession(trust_env=config.use_proxy)
self.api_helper = api_helpers.ApiHelper(config, self.web_session)
self.devices_discovered_dial = []
@@ -252,19 +252,13 @@ class AddDevice(ModalWithClickExit):
id="add-device-dial-button",
classes="button-switcher",
)
with ContentSwitcher(
id="add-device-switcher", initial="add-device-pin-container"
):
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)"
),
placeholder=("Pairing Code (found in Settings - Link with TV code)"),
id="pairing-code-input",
validators=[
Function(
_validate_pairing_code, "Invalid pairing code format"
)
Function(_validate_pairing_code, "Invalid pairing code format")
],
)
yield Input(
@@ -285,7 +279,8 @@ class AddDevice(ModalWithClickExit):
" computer\nIf it isn't showing up, try restarting the"
" app.\nIf running in docker, make sure to use"
" `--network=host`\nTo refresh the list, close and open the"
" dialog again"
" dialog again\n[b][u]If it still doesn't work, "
"pair using a pairing code (it's much more reliable)"
),
classes="subtitle",
)
@@ -307,7 +302,11 @@ class AddDevice(ModalWithClickExit):
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)
@@ -328,26 +327,25 @@ class AddDevice(ModalWithClickExit):
@on(Input.Changed, "#pairing-code-input")
def changed_pairing_code(self, event: Input.Changed):
self.query_one("#add-device-pin-add-button").disabled = (
not event.validation_result.is_valid
)
self.query_one("#add-device-pin-add-button").disabled = not event.validation_result.is_valid
@on(Input.Submitted, "#pairing-code-input")
@on(Button.Pressed, "#add-device-pin-add-button")
async def handle_add_device_pin(self) -> None:
self.query_one("#add-device-pin-add-button").disabled = True
lounge_controller = ytlounge.YtLoungeApi(
"iSponsorBlockTV", web_session=self.web_session
"iSponsorBlockTV",
)
await lounge_controller.change_web_session(self.web_session)
pairing_code = self.query_one("#pairing-code-input").value
pairing_code = int(
pairing_code.replace("-", "").replace(" ", "")
) # remove dashes and spaces
device_name = self.parent.query_one("#device-name-input").value
device_name = self.query_one("#device-name-input").value
paired = False
try:
paired = await lounge_controller.pair(pairing_code)
except:
except BaseException:
pass
if paired:
device = {
@@ -377,20 +375,19 @@ class AddDevice(ModalWithClickExit):
@on(SelectionList.SelectedChanged, "#dial-devices-list")
def changed_device_list(self, event: SelectionList.SelectedChanged):
self.query_one("#add-device-dial-add-button").disabled = (
not event.selection_list.selected
)
self.query_one("#add-device-dial-add-button").disabled = not event.selection_list.selected
class AddChannel(ModalWithClickExit):
"""Screen with a dialog to add a channel, either using search or with a channel id."""
"""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()
web_session = aiohttp.ClientSession(trust_env=config.use_proxy)
self.api_helper = api_helpers.ApiHelper(config, web_session)
def compose(self) -> ComposeResult:
@@ -416,9 +413,7 @@ class AddChannel(ModalWithClickExit):
classes="button-switcher",
)
yield Label(id="add-channel-info", classes="subtitle")
with ContentSwitcher(
id="add-channel-switcher", initial="add-channel-search-container"
):
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"):
@@ -426,9 +421,7 @@ class AddChannel(ModalWithClickExit):
placeholder="Enter channel name",
id="channel-name-input-search",
)
yield Button(
"Search", id="search-channel-button", variant="success"
)
yield Button("Search", id="search-channel-button", variant="success")
yield RadioSet(
RadioButton(label="Search to see results", disabled=True),
id="channel-search-results",
@@ -451,15 +444,12 @@ class AddChannel(ModalWithClickExit):
)
with Vertical(id="add-channel-id-container"):
yield Input(
placeholder=(
"Enter channel ID (example: UCuAXFkgsw1L7xaCfnd5JJOw)"
),
placeholder=("Enter channel ID (example: UCuAXFkgsw1L7xaCfnd5JJOw)"),
id="channel-id-input",
)
yield Input(
placeholder=(
"Enter channel name (only used to display in the config"
" file)"
"Enter channel name (only used to display in the config file)"
),
id="channel-name-input-id",
)
@@ -476,7 +466,6 @@ class AddChannel(ModalWithClickExit):
@on(Button.Pressed, "#add-channel-switch-buttons > *")
def handle_switch_buttons(self, event: Button.Pressed) -> None:
button_ = event.button.id
self.query_one("#add-channel-switcher").current = event.button.id.replace(
"-button", "-container"
)
@@ -486,9 +475,7 @@ class AddChannel(ModalWithClickExit):
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"
)
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...")
@@ -496,10 +483,8 @@ class AddChannel(ModalWithClickExit):
self.query_one("#channel-search-results").remove_children()
try:
channels_list = await self.api_helper.search_channels(channel_name)
except:
self.query_one("#add-channel-info").update(
"[#ff0000]Failed to search for channel"
)
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:
@@ -512,9 +497,7 @@ class AddChannel(ModalWithClickExit):
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"
)
self.query_one("#add-channel-info").update("[#ff0000]Please select a channel")
return
self.query_one("#add-channel-info").update("Adding...")
self.dismiss(channel)
@@ -526,9 +509,7 @@ class AddChannel(ModalWithClickExit):
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"
)
self.query_one("#add-channel-info").update("[#ff0000]Please enter a channel ID")
return
if not channel_name:
channel_name = channel_id
@@ -550,7 +531,7 @@ class EditDevice(ModalWithClickExit):
def action_close_screen_saving(self) -> None:
self.dismiss()
def dismiss(self) -> None:
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)
@@ -585,7 +566,7 @@ class EditDevice(ModalWithClickExit):
)
def on_slider_changed(self, event: Slider.Changed) -> None:
offset_input = self.query_one("#device-offset-offset_input")
offset_input = self.query_one("#device-offset-input")
with offset_input.prevent(Input.Changed):
offset_input.value = str(event.slider.value)
@@ -619,7 +600,7 @@ class DevicesManager(Vertical):
def compose(self) -> ComposeResult:
yield Label("Devices", classes="title")
with Horizontal(id="add-device-button-container"):
yield Button("Add Device", id="add-device", classes="button-100")
yield Button("Add Device", id="add-device", classes="button-100 button-small")
for device in self.devices:
yield Device(device, tooltip="Click to edit")
@@ -664,7 +645,7 @@ class ApiKeyManager(Vertical):
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"
" [link='https://console.developers.google.com/apis/credentials']Google Cloud"
" Console[/link]. This key is only required if you're whitelisting"
" channels."
)
@@ -683,7 +664,7 @@ class ApiKeyManager(Vertical):
@on(Button.Pressed, "#api-key-view")
def pressed_api_key_view(self, event: Button.Pressed):
if "Show" in event.button.label:
if "Show" in str(event.button.label):
event.button.label = "Hide key"
self.query_one("#api-key-input").password = False
else:
@@ -716,6 +697,43 @@ class SkipCategoriesManager(Vertical):
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."""
@@ -788,7 +806,8 @@ class AdSkipMuteManager(Vertical):
class ChannelWhitelistManager(Vertical):
"""Manager for channel whitelist, allows adding/removing channels from the whitelist."""
"""Manager for channel whitelist,
allows adding/removing channels from the whitelist."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
@@ -806,21 +825,18 @@ class ChannelWhitelistManager(Vertical):
id="channel-whitelist-subtitle",
)
yield Label(
(
":warning: [#FF0000]You need to set your YouTube Api Key in order to"
" use this feature"
),
("⚠️ [#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")
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)
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:
@@ -832,18 +848,18 @@ class ChannelWhitelistManager(Vertical):
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)
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)
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):
@@ -876,13 +892,45 @@ class AutoPlayManager(Vertical):
self.config.auto_play = event.checkbox.value
class ISponsorBlockTVSetupMainScreen(Screen):
"""Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221"""
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)
@@ -894,12 +942,15 @@ class ISponsorBlockTVSetupMainScreen(Screen):
yield Header()
yield Footer()
with ScrollableContainer(id="setup-wizard"):
yield DevicesManager(
config=self.config, id="devices-manager", classes="container"
)
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"
)
@@ -909,17 +960,13 @@ class ISponsorBlockTVSetupMainScreen(Screen):
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 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())
pass
def action_save(self) -> None:
self.config.save()
@@ -939,34 +986,11 @@ class ISponsorBlockTVSetupMainScreen(Screen):
@on(Input.Changed, "#api-key-input")
def changed_api_key(self, event: Input.Changed):
try: # ChannelWhitelist might not be mounted
# Show if no api key is set and at least one channel is in the whitelist
self.app.query_one("#warning-no-key").display = (
not event.input.value
) and self.config.channel_whitelist
except:
pass
class ISponsorBlockTVSetup(App):
CSS_PATH = ( # tcss is the recommended extension for textual css files
"setup-wizard-style.tcss"
self.app.query_one("#warning-no-key").display = bool(
(not event.input.value) and self.config.channel_whitelist
)
# Bindings for the whole app here, so they are available in all screens
BINDINGS = [("q,ctrl+c", "exit_modal", "Exit"), ("s", "save", "Save")]
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
self.main_screen = ISponsorBlockTVSetupMainScreen(config=self.config)
def on_mount(self) -> None:
self.push_screen(self.main_screen)
def action_save(self) -> None:
self.main_screen.action_save()
def action_exit_modal(self) -> None:
self.main_screen.action_exit_modal()
except NoMatches:
pass
def main(config):

View File

@@ -1,9 +1,14 @@
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
@@ -12,25 +17,25 @@ create_task = asyncio.create_task
class YtLoungeApi(pyytlounge.YtLoungeApi):
def __init__(
self,
screen_id,
screen_id=None,
config=None,
api_helper=None,
logger=None,
web_session: ClientSession = None,
):
super().__init__("iSponsorBlockTV", logger=logger)
if web_session is not None:
self.session = web_session # And use the one we passed
super().__init__(config.join_name if config else "iSponsorBlockTV", logger=logger)
self.auth.screen_id = screen_id
self.auth.lounge_id_token = None
self.api_helper = api_helper
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
@@ -39,37 +44,72 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
# Ensures that we still are subscribed to the lounge
async def _watchdog(self):
await asyncio.sleep(
35
) # YouTube sends at least a message every 30 seconds (no-op or any other)
"""
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()
except Exception:
pass
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
try:
# 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()
except:
pass # No watchdog task
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
def _process_event(self, event_id: int, event_type: str, args):
self.logger.debug(f"process_event({event_id}, {event_type}, {args})")
# (Re)start the watchdog
try:
self.subscribe_task_watchdog.cancel()
except:
pass
finally:
self.subscribe_task_watchdog = asyncio.create_task(self._watchdog())
# A bunch of events useful to detect ads playing, and the next video before it starts playing (that way we
# can get the segments)
# 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)
@@ -84,7 +124,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
create_task(self.mute(False, override=True))
elif event_type == "onAdStateChange":
data = args[0]
if data["adState"] == "0": # Ad is not playing
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 (
@@ -93,20 +133,16 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
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
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)
# 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]
pass
# Gets segments for the next video before it starts playing
elif event_type == "autoplayUpNext":
if len(args) > 0 and (
vid_id := args[0]["videoId"]
): # if video id is not empty
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))
@@ -117,15 +153,14 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
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))
elif (
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
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))
@@ -148,46 +183,167 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
elif event_type == "loungeScreenDisconnected":
if args: # Sometimes it's empty
data = args[0]
if (
data["reason"] == "disconnectedByUserScreenInitiated"
): # Short playing?
if data["reason"] == "disconnectedByUserScreenInitiated": # Short playing?
self.shorts_disconnected = True
elif event_type == "onAutoplayModeChanged":
create_task(self.set_auto_play_mode(self.auto_play))
super()._process_event(event_id, event_type, args)
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 super()._command("setVolume", {"volume": volume})
await self._command("setVolume", {"volume": volume})
# Mute or unmute the device (if the device already is in the desired state, nothing happens)
# mute: True to mute, False to unmute
# 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
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):
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 super()._command(
await self._command(
"setVolume",
{"volume": self.volume_state.get("volume", 100), "muted": mute_str},
)
async def set_auto_play_mode(self, enabled: bool):
await super()._command(
"setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"}
)
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