Add the ability to specify a custom API server

Fixes #193

Add the ability to specify a custom SponsorBlock API server. (draft implementation by copilot-workspace)

* Add a new configuration option `api_server` in `config.json.template` to specify the custom API server URL.
* Remove the hardcoded `SponsorBlock_api` URL from `src/iSponsorBlockTV/constants.py`.
* Update the `ApiHelper` class in `src/iSponsorBlockTV/api_helpers.py` to use the `api_server` configuration option for API calls.
* Add an option to input a custom API server URL in the CLI setup in `src/iSponsorBlockTV/config_setup.py`.
* Add an option to input a custom API server URL in the graphical setup wizard in `src/iSponsorBlockTV/setup_wizard.py`.
* Set the default `api_server` to "https://sponsor.ajay.app" in `src/iSponsorBlockTV/helpers.py`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/dmunozv04/iSponsorBlockTV/issues/193?shareId=XXXX-XXXX-XXXX-XXXX).
This commit is contained in:
David
2024-10-16 19:25:06 +02:00
parent d9ab2cd070
commit 1567a33e51
6 changed files with 234 additions and 200 deletions

View File

@@ -18,5 +18,6 @@
{"id": "", {"id": "",
"name": "" "name": ""
} }
] ],
"api_server": "https://sponsor.ajay.app"
} }

View File

@@ -27,6 +27,7 @@ class ApiHelper:
self.skip_count_tracking = config.skip_count_tracking self.skip_count_tracking = config.skip_count_tracking
self.web_session = web_session self.web_session = web_session
self.num_devices = len(config.devices) self.num_devices = len(config.devices)
self.api_server = config.api_server
# Not used anymore, maybe it can stay here a little longer # Not used anymore, maybe it can stay here a little longer
@AsyncLRU(maxsize=10) @AsyncLRU(maxsize=10)
@@ -130,7 +131,7 @@ class ApiHelper:
"service": constants.SponsorBlock_service, "service": constants.SponsorBlock_service,
} }
headers = {"Accept": "application/json"} headers = {"Accept": "application/json"}
url = constants.SponsorBlock_api + "skipSegments/" + vid_id_hashed url = self.api_server + "/api/skipSegments/" + vid_id_hashed
async with self.web_session.get( async with self.web_session.get(
url, headers=headers, params=params url, headers=headers, params=params
) as response: ) as response:
@@ -201,7 +202,7 @@ class ApiHelper:
Lets the contributor know that someone skipped the segment (thanks)""" Lets the contributor know that someone skipped the segment (thanks)"""
if self.skip_count_tracking: if self.skip_count_tracking:
for i in uuids: for i in uuids:
url = constants.SponsorBlock_api + "viewedVideoSponsorTime/" url = self.api_server + "/api/viewedVideoSponsorTime/"
params = {"UUID": i} params = {"UUID": i}
await self.web_session.post(url, params=params) await self.web_session.post(url, params=params)

View File

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

View File

@@ -2,7 +2,6 @@ userAgent = "iSponsorBlockTV/0.1"
SponsorBlock_service = "youtube" SponsorBlock_service = "youtube"
SponsorBlock_actiontype = "skip" SponsorBlock_actiontype = "skip"
SponsorBlock_api = "https://sponsor.ajay.app/api/"
Youtube_api = "https://www.googleapis.com/youtube/v3/" Youtube_api = "https://www.googleapis.com/youtube/v3/"
skip_categories = ( skip_categories = (
@@ -20,5 +19,4 @@ skip_categories = (
youtube_client_blacklist = ["TVHTML5_FOR_KIDS"] youtube_client_blacklist = ["TVHTML5_FOR_KIDS"]
config_file_blacklist_keys = ["config_file", "data_dir"] config_file_blacklist_keys = ["config_file", "data_dir"]

View File

@@ -42,6 +42,7 @@ class Config:
self.mute_ads = False self.mute_ads = False
self.skip_ads = False self.skip_ads = False
self.auto_play = True self.auto_play = True
self.api_server = "https://sponsor.ajay.app"
self.__load() self.__load()
def validate(self): def validate(self):

View File

@@ -876,6 +876,31 @@ class AutoPlayManager(Vertical):
self.config.auto_play = event.checkbox.value self.config.auto_play = event.checkbox.value
class ApiServerManager(Vertical):
"""Manager for the custom API server URL."""
def __init__(self, config, **kwargs) -> None:
super().__init__(**kwargs)
self.config = config
def compose(self) -> ComposeResult:
yield Label("Custom API Server", classes="title")
yield Label(
"You can specify a custom SponsorBlock API server URL here.",
classes="subtitle",
)
with Grid(id="api-server-grid"):
yield Input(
placeholder="Custom API Server URL",
id="api-server-input",
value=self.config.api_server,
)
@on(Input.Changed, "#api-server-input")
def changed_api_server(self, event: Input.Changed):
self.config.api_server = event.input.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"""
@@ -915,6 +940,9 @@ class ISponsorBlockTVSetupMainScreen(Screen):
yield AutoPlayManager( yield AutoPlayManager(
config=self.config, id="autoplay-manager", classes="container" config=self.config, id="autoplay-manager", classes="container"
) )
yield ApiServerManager(
config=self.config, id="api-server-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():