Compare commits

...

39 Commits

Author SHA1 Message Date
pre-commit-ci[bot]
7d3b1c6469 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-08-18 15:38:13 +00:00
dmunozv04
b7a295b3e2 create service handlers for macOS 2024-08-18 17:36:29 +02:00
David
c56cbfe095 Merge pull request #185 from dmunozv04/build-binaries
Build binaries
2024-08-18 17:30:34 +02:00
pre-commit-ci[bot]
bde4ecb72f [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-08-18 15:23:15 +00:00
dmunozv04
167383dea8 revert 2024-08-18 16:11:54 +02:00
dmunozv04
fb3c40d477 also build armv7 2024-08-18 15:58:15 +02:00
dmunozv04
cb738965a7 run on all events (and only publish on release) 2024-08-18 15:07:35 +02:00
dmunozv04
9ad335793a rename prefix 2024-08-18 14:35:02 +02:00
dmunozv04
fd400d077a update upload-artifact to v4 2024-08-18 14:19:39 +02:00
dmunozv04
f9c7b58ece add needs 2024-08-18 14:12:49 +02:00
dmunozv04
464baa7c59 trigger on test branch 2024-08-18 14:07:47 +02:00
dmunozv04
d9986e52b3 Merge remote-tracking branch 'origin/main' into build-binaries 2024-08-18 14:07:19 +02:00
dmunozv04
547a47b9ec modify action to create and publish binaries 2024-08-18 14:04:34 +02:00
dmunozv04
87d0e0e32e rework cli 2024-08-18 14:04:04 +02:00
David
854cb2462f Merge pull request #171 from guoard/dockerfile-issues
Fix buildx warnings when creating the docker image
2024-08-16 12:42:41 +02:00
David
662b71fc00 Merge pull request #177 from AN1MATEK/patch-1
Update config.json.template to correct the syntax error in auto_play
2024-07-14 11:12:08 +02:00
ANIMATEK
fd6b7cb43a Update config.json.template
Correct the error syntax from `autoplay` to the actual variable `auto_play`
2024-07-14 11:09:41 +02:00
dmunozv04
d17e59bf0d bump version 2024-07-07 17:36:01 +02:00
Ali Afsharzadeh
5bc6382f89 Fix buildx warnings when creating the docker image 2024-06-29 09:28:24 +03:30
David
205191f442 Merge pull request #150 from ryankupk/update-cli
Refactor CLI setup script, add prompts for muting and skipping native youtube ads
2024-06-21 17:08:47 +02:00
dmunozv04
810cd5eec3 change default options for autoplay and reporting, and mark default option 2024-06-21 17:08:15 +02:00
pre-commit-ci[bot]
e2ace8629f [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-06-21 15:01:56 +00:00
dmunozv04
e54ead26d2 Merge remote-tracking branch 'origin' into pr/ryankupk/150 2024-06-21 17:01:45 +02:00
David
49fba2f28f Merge pull request #161 from Ravioli8235/autoplay
Implement autoplay on/off toggle
2024-06-21 16:56:57 +02:00
pre-commit-ci[bot]
b1333a2f61 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-06-21 14:29:35 +00:00
dmunozv04
cfef219d32 fix autoplay padding 2024-06-21 16:29:17 +02:00
Ravioli8235
338e0479ba Merge remote-tracking branch 'origin/autoplay' into autoplay 2024-06-21 15:58:28 +02:00
Ravioli8235
bfefa94a7b fix panel fill 2024-06-21 15:58:15 +02:00
David
783e3d4240 Merge branch 'main' into autoplay 2024-06-17 14:55:07 +02:00
David
015f5a79c9 Merge pull request #168 from Shraymonks/fix-docker-root
Fix deps permissions in docker build
2024-06-16 14:22:25 +02:00
Raymond Ha
dc72db0609 Fix deps permissions in docker build 2024-06-05 16:18:10 -07:00
David
8de38cc92b Merge pull request #167 from dmunozv04/update-actions
Update actions versions
2024-06-05 19:29:42 +02:00
dmunozv04
94ba642af1 update actions versions to latest 2024-06-05 19:27:42 +02:00
David
d3341009a6 Merge branch 'main' into autoplay 2024-05-30 12:27:47 +02:00
David
5dbd16ddd5 Merge branch 'main' into autoplay 2024-05-30 09:05:18 +02:00
pre-commit-ci[bot]
faa0379b89 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-05-30 07:03:27 +00:00
Ravioli8235
fb3ed6b39a Implement autoplay 2024-05-30 08:53:35 +02:00
pre-commit-ci[bot]
865f5469a2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-05-03 16:24:21 +00:00
Ryan Kupka
daa7026221 Refactor CLI setup script, add prompts for muting and skipping native youtube ads 2024-05-03 10:18:16 -06:00
17 changed files with 726 additions and 312 deletions

View File

@@ -25,12 +25,12 @@ jobs:
steps: steps:
# Get the repository's code # Get the repository's code
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
# Generate docker tags # Generate docker tags
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: ghcr.io/dmunozv04/isponsorblocktv, dmunozv04/isponsorblocktv images: ghcr.io/dmunozv04/isponsorblocktv, dmunozv04/isponsorblocktv
tags: | tags: |
@@ -42,29 +42,29 @@ jobs:
# https://github.com/docker/setup-qemu-action # https://github.com/docker/setup-qemu-action
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
# https://github.com/docker/setup-buildx-action # https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to DockerHub - name: Login to DockerHub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR - name: Login to GHCR
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64, linux/arm64, linux/arm/v7 platforms: linux/amd64, linux/arm64, linux/arm/v7

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

@@ -0,0 +1,196 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Release Package
on:
push:
branches:
- '*'
tags:
- 'v*'
pull_request:
branches:
- '*'
release:
types: [published]
defaults:
run:
shell: bash
env:
PYTHON_VERSION: "3.11"
permissions:
contents: read
jobs:
build-sdist-and-wheel:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch
- name: Build package
run: python -m hatch build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: sdist-and-wheel
path: dist/*
if-no-files-found: error
build-binaries:
name: Build binaries for ${{ matrix.job.target }} (${{ matrix.job.os }})
needs:
- build-sdist-and-wheel
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
# Linux
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
cross: true
release_suffix: x86_64-linux
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
cross: true
release_suffix: aarch64-linux
# Windows
- target: x86_64-pc-windows-msvc
os: windows-2022
release_suffix: x86_64-windows
# macOS
- target: aarch64-apple-darwin
os: macos-14
release_suffix: aarch64-osx
- target: x86_64-apple-darwin
os: macos-12
release_suffix: x86_64-osx
env:
PYAPP_PASS_LOCATION: "1"
PYAPP_UV_ENABLED: "1"
HATCH_BUILD_LOCATION: dist
CARGO: cargo
CARGO_BUILD_TARGET: ${{ matrix.job.target }}
PYAPP_REPO: pyapp # Use local copy of pyapp (needed for cross-compiling)
PYAPP_VERSION: v0.23.0
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Clone PyApp
run: git clone --depth 1 --branch $PYAPP_VERSION https://github.com/ofek/pyapp $PYAPP_REPO
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install Hatch
run: |
python -m pip install --upgrade pip
pip install hatch
- name: Install Rust toolchain
if: ${{ !matrix.job.cross }}
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.job.target }}
- name: Set up cross compiling tools
if: matrix.job.cross
uses: taiki-e/setup-cross-toolchain-action@v1
with:
target: ${{ matrix.job.target}}
- name: Show toolchain information
run: |-
rustup toolchain list
rustup default
rustup -V
rustc -V
cargo -V
hatch --version
- name: Get artifact
uses: actions/download-artifact@v4
with:
name: sdist-and-wheel
path: ${{ github.workspace }}/dist
merge-multiple: true
- name: Build Binary
working-directory: ${{ github.workspace }}
run: |-
current_version=$(hatch version)
PYAPP_PROJECT_PATH="${{ github.workspace }}/dist/isponsorblocktv-${current_version}-py3-none-any.whl" hatch -v build -t binary
- name: Rename binary
working-directory: ${{ github.workspace }}
run: |-
mv dist/binary/iSponsorBlockTV* dist/binary/iSponsorBlockTV-${{ matrix.job.release_suffix }}
- name: Upload built binary package
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.job.release_suffix }}
path: dist/binary/*
if-no-files-found: error
publish-to-pypi:
needs: build-sdist-and-wheel
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
# only run step if the event is a published release
if: github.event_name == 'release' && github.event.action == 'published'
runs-on: ubuntu-latest
steps:
- name: Get artifact
uses: actions/download-artifact@v4
with:
name: sdist-and-wheel
path: dist
merge-multiple: true
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
publish-to-release:
needs:
- build-sdist-and-wheel
- build-binaries
if: github.event_name == 'release' && github.event.action == 'published'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
name: Get artifact
with:
path: dist
merge-multiple: true
- name: Add assets to release
uses: softprops/action-gh-release@v2
with:
files: dist/*

View File

@@ -1,37 +0,0 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build wheel
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1

View File

@@ -22,7 +22,7 @@ jobs:
# Update description # Update description
- name: Update repo description - name: Update repo description
uses: peter-evans/dockerhub-description@v3 uses: peter-evans/dockerhub-description@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM python:3.11-alpine3.19 as BASE FROM python:3.11-alpine3.19 AS base
FROM base as compiler FROM base AS compiler
WORKDIR /app WORKDIR /app
@@ -10,18 +10,18 @@ COPY src .
RUN python3 -m compileall -b -f . && \ RUN python3 -m compileall -b -f . && \
find . -name "*.py" -type f -delete find . -name "*.py" -type f -delete
FROM base as DEP_INSTALLER FROM base AS dep_installer
COPY requirements.txt . COPY requirements.txt .
RUN apk add --no-cache gcc musl-dev && \ RUN apk add --no-cache gcc musl-dev && \
pip install --upgrade pip wheel && \ pip install --upgrade pip wheel && \
pip install --user -r requirements.txt && \ pip install -r requirements.txt && \
pip uninstall -y pip wheel && \ pip uninstall -y pip wheel && \
apk del gcc musl-dev && \ apk del gcc musl-dev && \
python3 -m compileall -b -f /root/.local/lib/python3.11/site-packages && \ python3 -m compileall -b -f /usr/local/lib/python3.11/site-packages && \
find /root/.local/lib/python3.11/site-packages -name "*.py" -type f -delete && \ find /usr/local/lib/python3.11/site-packages -name "*.py" -type f -delete && \
find /root/.local/lib/python3.11/ -name "__pycache__" -type d -exec rm -rf {} + find /usr/local/lib/python3.11/ -name "__pycache__" -type d -exec rm -rf {} +
FROM base FROM base
@@ -29,7 +29,7 @@ ENV PIP_NO_CACHE_DIR=off iSPBTV_docker=True iSPBTV_data_dir=data TERM=xterm-256c
COPY requirements.txt . COPY requirements.txt .
COPY --from=dep_installer /root/.local /root/.local COPY --from=dep_installer /usr/local /usr/local
WORKDIR /app WORKDIR /app

View File

@@ -12,6 +12,7 @@
"skip_count_tracking": true, "skip_count_tracking": true,
"mute_ads": true, "mute_ads": true,
"skip_ads": true, "skip_ads": true,
"auto_play": true,
"apikey": "", "apikey": "",
"channel_whitelist": [ "channel_whitelist": [
{"id": "", {"id": "",

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "iSponsorBlockTV" name = "iSponsorBlockTV"
version = "2.0.8" version = "2.1.0"
authors = [ authors = [
{"name" = "dmunozv04"} {"name" = "dmunozv04"}
] ]

View File

@@ -8,3 +8,4 @@ ssdp==1.3.0
textual==0.58.0 textual==0.58.0
textual-slider==0.1.1 textual-slider==0.1.1
xmltodict==0.13.0 xmltodict==0.13.0
rich_click==1.8.3

View File

@@ -1,169 +1,195 @@
import asyncio import asyncio
import aiohttp import aiohttp
from . import api_helpers, ytlounge from . import api_helpers, ytlounge
# Constants for user input prompts
async def pair_device(web_session): ATVS_REMOVAL_PROMPT = (
try: "Do you want to remove the legacy 'atvs' entry (the app won't start"
lounge_controller = ytlounge.YtLoungeApi( " with it present)? (y/N) "
"iSponsorBlockTV", web_session=web_session )
) PAIRING_CODE_PROMPT = "Enter pairing code (found in Settings - Link with TV code): "
pairing_code = input( ADD_MORE_DEVICES_PROMPT = "Paired with {num_devices} Device(s). Add more? (y/N) "
"Enter pairing code (found in Settings - Link with TV code): " CHANGE_API_KEY_PROMPT = "API key already specified. Change it? (y/N) "
) ADD_API_KEY_PROMPT = (
pairing_code = int( "API key only needed for the channel whitelist function. Add it? (y/N) "
pairing_code.replace("-", "").replace(" ", "") )
) # remove dashes and spaces ENTER_API_KEY_PROMPT = "Enter your API key: "
print("Pairing...") CHANGE_SKIP_CATEGORIES_PROMPT = "Skip categories already specified. Change them? (y/N) "
paired = await lounge_controller.pair(pairing_code) ENTER_SKIP_CATEGORIES_PROMPT = (
if not paired: "Enter skip categories (space or comma sepparated) Options: [sponsor,"
print("Failed to pair device") " selfpromo, exclusive_access, interaction, poi_highlight, intro, outro,"
return " preview, filler, music_offtopic]:\n"
device = { )
"screen_id": lounge_controller.auth.screen_id, WHITELIST_CHANNELS_PROMPT = (
"name": lounge_controller.screen_name, "Do you want to whitelist any channels from being ad-blocked? (y/N) "
} )
print(f"Paired device: {device['name']}") SEARCH_CHANNEL_PROMPT = 'Enter a channel name or "/exit" to exit: '
return device SELECT_CHANNEL_PROMPT = "Select one option of the above [0-6]: "
except Exception as e: ENTER_CHANNEL_ID_PROMPT = "Enter a channel ID: "
print(f"Failed to pair device: {e}") ENTER_CUSTOM_CHANNEL_NAME_PROMPT = "Enter the channel name: "
return REPORT_SKIPPED_SEGMENTS_PROMPT = (
"Do you want to report skipped segments to sponsorblock. Only the segment"
" UUID will be sent? (Y/n) "
def main(config, debug: bool) -> None: )
print("Welcome to the iSponsorBlockTV cli setup wizard") MUTE_ADS_PROMPT = "Do you want to mute native YouTube ads automatically? (y/N) "
loop = asyncio.get_event_loop_policy().get_event_loop() SKIP_ADS_PROMPT = "Do you want to skip native YouTube ads automatically? (y/N) "
web_session = aiohttp.ClientSession() AUTOPLAY_PROMPT = "Do you want to enable autoplay? (Y/n) "
if debug:
loop.set_debug(True)
asyncio.set_event_loop(loop) def get_yn_input(prompt):
if hasattr(config, "atvs"): while choice := input(prompt):
print( if choice.lower() in ["y", "n"]:
"The atvs config option is deprecated and has stopped working. Please read" return choice.lower()
" this for more information on how to upgrade to V2:" print("Invalid input. Please enter 'y' or 'n'.")
" \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2"
)
if ( async def pair_device():
input( try:
"Do you want to remove the legacy 'atvs' entry (the app won't start" lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV")
" with it present)? (y/n) " pairing_code = input(PAIRING_CODE_PROMPT)
) pairing_code = int(
== "y" pairing_code.replace("-", "").replace(" ", "")
): ) # remove dashes and spaces
del config["atvs"] print("Pairing...")
devices = config.devices paired = await lounge_controller.pair(pairing_code)
while not input(f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n": if not paired:
task = loop.create_task(pair_device(web_session)) print("Failed to pair device")
loop.run_until_complete(task) return
device = task.result() device = {
if device: "screen_id": lounge_controller.auth.screen_id,
devices.append(device) "name": lounge_controller.screen_name,
config.devices = devices }
print(f"Paired device: {device['name']}")
apikey = config.apikey return device
if apikey: except Exception as e:
if input("API key already specified. Change it? (y/n) ") == "y": print(f"Failed to pair device: {e}")
apikey = input("Enter your API key: ") return
else:
if (
input( def main(config, debug: bool) -> None:
"API key only needed for the channel whitelist function. Add it? (y/n) " print("Welcome to the iSponsorBlockTV cli setup wizard")
) loop = asyncio.get_event_loop_policy().get_event_loop()
== "y" web_session = aiohttp.ClientSession()
): if debug:
print( loop.set_debug(True)
"Get youtube apikey here:" asyncio.set_event_loop(loop)
" https://developers.google.com/youtube/registering_an_application" if hasattr(config, "atvs"):
) print(
apikey = input("Enter your API key: ") "The atvs config option is deprecated and has stopped working. Please read"
config.apikey = apikey " this for more information on how to upgrade to V2:"
" \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2"
skip_categories = config.skip_categories )
if skip_categories: choice = get_yn_input(ATVS_REMOVAL_PROMPT)
if input("Skip categories already specified. Change them? (y/n) ") == "y": if choice == "y":
categories = input( del config["atvs"]
"Enter skip categories (space or comma sepparated) Options: [sponsor"
" selfpromo exclusive_access interaction poi_highlight intro outro" devices = config.devices
" preview filler music_offtopic]:\n" choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices)))
) while choice == "y":
skip_categories = categories.replace(",", " ").split(" ") task = loop.create_task(pair_device())
skip_categories = [ loop.run_until_complete(task)
x for x in skip_categories if x != "" device = task.result()
] # Remove empty strings if device:
else: devices.append(device)
categories = input( choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices)))
"Enter skip categories (space or comma sepparated) Options: [sponsor," config.devices = devices
" selfpromo, exclusive_access, interaction, poi_highlight, intro, outro,"
" preview, filler, music_offtopic:\n" apikey = config.apikey
) if apikey:
skip_categories = categories.replace(",", " ").split(" ") choice = get_yn_input(CHANGE_API_KEY_PROMPT)
skip_categories = [ if choice == "y":
x for x in skip_categories if x != "" apikey = input(ENTER_API_KEY_PROMPT)
] # Remove empty strings else:
config.skip_categories = skip_categories choice = get_yn_input(ADD_API_KEY_PROMPT)
if choice == "y":
channel_whitelist = config.channel_whitelist print(
if ( "Get youtube apikey here:"
input("Do you want to whitelist any channels from being ad-blocked? (y/n) ") " https://developers.google.com/youtube/registering_an_application"
== "y" )
): apikey = input(ENTER_API_KEY_PROMPT)
if not apikey: config.apikey = apikey
print(
"WARNING: You need to specify an API key to use this function," skip_categories = config.skip_categories
" otherwise the program will fail to start.\nYou can add one by" if skip_categories:
" re-running this setup wizard." choice = get_yn_input(CHANGE_SKIP_CATEGORIES_PROMPT)
) if choice == "y":
api_helper = api_helpers.ApiHelper(config, web_session) categories = input(ENTER_SKIP_CATEGORIES_PROMPT)
while True: skip_categories = categories.replace(",", " ").split(" ")
channel_info = {} skip_categories = [
channel = input('Enter a channel name or "/exit" to exit: ') x for x in skip_categories if x != ""
if channel == "/exit": ] # Remove empty strings
break else:
categories = input(ENTER_SKIP_CATEGORIES_PROMPT)
task = loop.create_task( skip_categories = categories.replace(",", " ").split(" ")
api_helper.search_channels(channel, apikey, web_session) skip_categories = [
) x for x in skip_categories if x != ""
loop.run_until_complete(task) ] # Remove empty strings
results = task.result() config.skip_categories = skip_categories
if len(results) == 0:
print("No channels found") channel_whitelist = config.channel_whitelist
continue choice = get_yn_input(WHITELIST_CHANNELS_PROMPT)
if choice == "y":
for i, item in enumerate(results): if not apikey:
print(f"{i}: {item[1]} - Subs: {item[2]}") print(
print("5: Enter a custom channel ID") "WARNING: You need to specify an API key to use this function,"
print("6: Go back") " otherwise the program will fail to start.\nYou can add one by"
" re-running this setup wizard."
choice = -1 )
choice = input("Select one option of the above [0-6]: ") api_helper = api_helpers.ApiHelper(config, web_session)
while choice not in [str(x) for x in range(7)]: while True:
print("Invalid choice") channel_info = {}
choice = input("Select one option of the above [0-6]: ") channel = input(SEARCH_CHANNEL_PROMPT)
if channel == "/exit":
if choice == "5": break
channel_info["id"] = input("Enter a channel ID: ")
channel_info["name"] = input("Enter the channel name: ") task = loop.create_task(
channel_whitelist.append(channel_info) api_helper.search_channels(channel, apikey, web_session)
continue )
if choice == "6": loop.run_until_complete(task)
continue results = task.result()
if len(results) == 0:
channel_info["id"] = results[int(choice)][0] print("No channels found")
channel_info["name"] = results[int(choice)][1] continue
channel_whitelist.append(channel_info)
# Close web session asynchronously for i, item in enumerate(results):
print(f"{i}: {item[1]} - Subs: {item[2]}")
config.channel_whitelist = channel_whitelist print("5: Enter a custom channel ID")
print("6: Go back")
config.skip_count_tracking = (
not input( while choice := input(SELECT_CHANNEL_PROMPT):
"Do you want to report skipped segments to sponsorblock. Only the segment" if choice in [str(x) for x in range(7)]:
" UUID will be sent? (y/n) " break
) print("Invalid choice")
== "n"
) if choice == "5":
print("Config finished") channel_info["id"] = input(ENTER_CHANNEL_ID_PROMPT)
config.save() channel_info["name"] = input(ENTER_CUSTOM_CHANNEL_NAME_PROMPT)
loop.run_until_complete(web_session.close()) channel_whitelist.append(channel_info)
continue
if choice == "6":
continue
channel_info["id"] = results[int(choice)][0]
channel_info["name"] = results[int(choice)][1]
channel_whitelist.append(channel_info)
# Close web session asynchronously
config.channel_whitelist = channel_whitelist
choice = get_yn_input(REPORT_SKIPPED_SEGMENTS_PROMPT)
config.skip_count_tracking = choice != "n"
choice = get_yn_input(MUTE_ADS_PROMPT)
config.mute_ads = choice == "y"
choice = get_yn_input(SKIP_ADS_PROMPT)
config.skip_ads = choice == "y"
choice = get_yn_input(AUTOPLAY_PROMPT)
config.auto_play = choice != "n"
print("Config finished")
config.save()
loop.run_until_complete(web_session.close())

View File

@@ -1,14 +1,15 @@
import argparse
import json import json
import logging import logging
import os import os
import sys import sys
import time import time
import rich_click as click
from appdirs import user_data_dir from appdirs import user_data_dir
from . import config_setup, main, setup_wizard from . import config_setup, main, setup_wizard
from .constants import config_file_blacklist_keys from .constants import config_file_blacklist_keys
from .service.service_helpers import service
class Device: class Device:
@@ -41,6 +42,7 @@ class Config:
self.skip_count_tracking = True self.skip_count_tracking = True
self.mute_ads = False self.mute_ads = False
self.skip_ads = False self.skip_ads = False
self.auto_play = True
self.__load() self.__load()
def validate(self): def validate(self):
@@ -125,35 +127,95 @@ class Config:
return False return False
def app_start(): @click.group(invoke_without_command=True)
# If env has a data dir use that, otherwise use the default @click.option(
default_data_dir = os.getenv("iSPBTV_data_dir") or user_data_dir( "--data",
"iSponsorBlockTV", "dmunozv04" "-d",
) default=lambda: os.getenv("iSPBTV_data_dir")
parser = argparse.ArgumentParser(description="iSponsorblockTV") or user_data_dir("iSponsorBlockTV", "dmunozv04"),
parser.add_argument( help="data directory",
"--data-dir", "-d", default=default_data_dir, help="data directory" )
) @click.option("--debug", is_flag=True, help="debug mode")
parser.add_argument( # legacy commands as arguments
"--setup", "-s", action="store_true", help="setup the program graphically" @click.option(
) "--setup", is_flag=True, help="Setup the program graphically", hidden=True
parser.add_argument( )
"--setup-cli", @click.option(
"-sc", "--setup-cli",
action="store_true", is_flag=True,
help="setup the program in the command line", help="Setup the program in the command line",
) hidden=True,
parser.add_argument("--debug", action="store_true", help="debug mode") )
args = parser.parse_args() @click.pass_context
def cli(ctx, data, debug, setup, setup_cli):
config = Config(args.data_dir) """iSponsorblockTV"""
if args.debug: ctx.ensure_object(dict)
ctx.obj["data_dir"] = data
ctx.obj["debug"] = debug
if debug:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
if args.setup: # Set up the config file graphically if ctx.invoked_subcommand is None:
setup_wizard.main(config) if setup:
sys.exit() ctx.invoke(setup_command)
if args.setup_cli: # Set up the config file elif setup_cli:
config_setup.main(config, args.debug) ctx.invoke(setup_cli_command)
else: else:
config.validate() ctx.invoke(start)
main.main(config, args.debug)
@cli.command()
@click.pass_context
def setup(ctx):
"""Setup the program graphically"""
config = Config(ctx.obj["data_dir"])
setup_wizard.main(config)
sys.exit()
setup_command = setup
@cli.command()
@click.pass_context
def setup_cli(ctx):
"""Setup the program in the command line"""
config = Config(ctx.obj["data_dir"])
config_setup.main(config, ctx.obj["debug"])
setup_cli_command = setup_cli
@cli.command()
@click.pass_context
def start(ctx):
"""Start the main program"""
config = Config(ctx.obj["data_dir"])
config.validate()
main.main(config, ctx.obj["debug"])
# Create fake "self" group to show pyapp options in help menu
# Subcommands remove, restore, update
pyapp_group = click.RichGroup("self", help="pyapp options (update, remove, restore)")
pyapp_group.add_command(
click.RichCommand("update", help="Update the package to the latest version")
)
pyapp_group.add_command(
click.Command(
"remove", help="Remove the package, wiping the installation but not the data"
)
)
pyapp_group.add_command(
click.RichCommand(
"restore", help="Restore the package to its original state by reinstalling it"
)
)
if os.getenv("PYAPP"):
cli.add_command(pyapp_group)
cli.add_command(service)
def app_start():
cli(obj={})

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

View File

@@ -0,0 +1,76 @@
import os
import sys
import rich_click as click
from .service_managers import select_service_manager
@click.group()
@click.pass_context
def service(ctx):
"""Manage the program as a service (executable only)"""
ctx.ensure_object(dict)
if os.getenv("PYAPP") is None:
print(
"Service commands are only available in the executable version of the"
" program"
)
sys.exit(1)
ctx.obj["service_manager"] = select_service_manager()(os.getenv("PYAPP"))
@service.command()
@click.pass_context
def start(ctx):
"""Start the service"""
ctx.obj["service_manager"].start()
@service.command()
@click.pass_context
def stop(ctx):
"""Stop the service"""
ctx.obj["service_manager"].stop()
@service.command()
@click.pass_context
def restart(ctx):
"""Restart the service"""
ctx.obj["service_manager"].restart()
@service.command()
@click.pass_context
def status(ctx):
"""Get the status of the service"""
ctx.obj["service_manager"].status()
@service.command()
@click.pass_context
def install(ctx):
"""Install the service"""
ctx.obj["service_manager"].install()
@service.command()
@click.pass_context
def uninstall(ctx):
"""Uninstall the service"""
ctx.obj["service_manager"].uninstall()
@service.command()
@click.pass_context
def enable(ctx):
"""Enable the service"""
ctx.obj["service_manager"].enable()
@service.command()
@click.pass_context
def disable(ctx):
"""Disable the service"""
ctx.obj["service_manager"].disable()

View File

@@ -0,0 +1,107 @@
import os
import plistlib
import subprocess
from platform import system
from appdirs import user_log_dir
def select_service_manager() -> "ServiceManager":
platform = system()
if platform == "Darwin":
return Launchd
elif platform == "Linux":
return Systemd
else:
raise NotImplementedError("Unsupported platform")
class ServiceManager:
def __init__(self, executable_path, *args, **kwargs):
self.executable_path = executable_path
def start(self):
pass
def stop(self):
pass
def restart(self):
pass
def status(self):
pass
def install(self):
pass
def uninstall(self):
pass
def enable(self):
pass
def disable(self):
pass
class Launchd(ServiceManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.service_name = "com.dmunozv04.iSponsorBlockTV"
self.service_path = (
os.path.expanduser("~/Library/LaunchAgents/") + self.service_name + ".plist"
)
def start(self):
subprocess.run(["launchctl", "start", self.service_name])
def stop(self):
subprocess.run(["launchctl", "stop", self.service_name])
def restart(self):
subprocess.run(["launchctl", "restart", self.service_name])
def status(self):
subprocess.run(["launchctl", "list", self.service_name])
def install(self):
if os.path.exists(self.service_path):
print("Service already installed")
return
logs_dir = user_log_dir("iSponsorBlockTV", "dmunozv04")
# ensure the logs directory exists
os.makedirs(logs_dir, exist_ok=True)
plist = {
"Label": "com.dmunozv04.iSponsorBlockTV",
"RunAtLoad": True,
"StartInterval": 20,
"EnvironmentVariables": {"PYTHONUNBUFFERED": "YES"},
"StandardErrorPath": logs_dir + "/iSponsorBlockTV.err",
"StandardOutPath": logs_dir + "/iSponsorBlockTV.out",
"Program": self.executable_path,
}
with open(self.service_path, "wb") as fp:
plistlib.dump(plist, fp)
print("Service installed")
self.enable()
def uninstall(self):
self.disable()
# Remove the file
try:
os.remove(self.service_path)
print("Service uninstalled")
except FileNotFoundError:
print("Service not found")
def enable(self):
subprocess.run(["launchctl", "load", self.service_path])
def disable(self):
subprocess.run(["launchctl", "stop", self.service_name])
subprocess.run(["launchctl", "unload", self.service_path])
class Systemd(ServiceManager):
pass

View File

@@ -363,3 +363,9 @@ MigrationScreen {
width: 1fr; width: 1fr;
content-align: center middle; content-align: center middle;
} }
/* Autoplay */
#autoplay-container{
padding: 1;
height: auto;
}

View File

@@ -850,6 +850,32 @@ class ChannelWhitelistManager(Vertical):
self.app.push_screen(AddChannel(self.config), callback=self.new_channel) self.app.push_screen(AddChannel(self.config), callback=self.new_channel)
class AutoPlayManager(Vertical):
"""Manager for autoplay, allows enabling/disabling autoplay."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Autoplay", classes="title")
yield Label(
"This feature allows you to enable/disable autoplay",
classes="subtitle",
id="autoplay-subtitle",
)
with Horizontal(id="autoplay-container"):
yield Checkbox(
value=self.config.auto_play,
id="autoplay-switch",
label="Enable autoplay",
)
@on(Checkbox.Changed, "#autoplay-switch")
def changed_skip(self, event: Checkbox.Changed):
self.config.auto_play = event.checkbox.value
class ISponsorBlockTVSetupMainScreen(Screen): class ISponsorBlockTVSetupMainScreen(Screen):
"""Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221""" """Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221"""
@@ -886,6 +912,9 @@ class ISponsorBlockTVSetupMainScreen(Screen):
yield ApiKeyManager( yield ApiKeyManager(
config=self.config, id="api-key-manager", classes="container" config=self.config, id="api-key-manager", classes="container"
) )
yield AutoPlayManager(
config=self.config, id="autoplay-manager", classes="container"
)
def on_mount(self) -> None: def on_mount(self) -> None:
if self.check_for_old_config_entries(): if self.check_for_old_config_entries():

View File

@@ -30,9 +30,11 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
self.callback = None self.callback = None
self.logger = logger self.logger = logger
self.shorts_disconnected = False self.shorts_disconnected = False
self.auto_play = True
if config: if config:
self.mute_ads = config.mute_ads self.mute_ads = config.mute_ads
self.skip_ads = config.skip_ads self.skip_ads = config.skip_ads
self.auto_play = config.auto_play
# Ensures that we still are subscribed to the lounge # Ensures that we still are subscribed to the lounge
async def _watchdog(self): async def _watchdog(self):
@@ -146,6 +148,8 @@ class YtLoungeApi(pyytlounge.YtLoungeApi):
data = args[0] data = args[0]
if data["reason"] == "disconnectedByUserScreenInitiated": # Short playing? if data["reason"] == "disconnectedByUserScreenInitiated": # Short playing?
self.shorts_disconnected = True self.shorts_disconnected = True
elif event_type == "onAutoplayModeChanged":
create_task(self.set_auto_play_mode(self.auto_play))
super()._process_event(event_id, event_type, args) super()._process_event(event_id, event_type, args)