From 1567a33e516326bfeec5f067e12594b03625fe61 Mon Sep 17 00:00:00 2001 From: David <39565245+dmunozv04@users.noreply.github.com> Date: Wed, 16 Oct 2024 19:25:06 +0200 Subject: [PATCH 1/2] 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). --- config.json.template | 3 +- src/iSponsorBlockTV/api_helpers.py | 5 +- src/iSponsorBlockTV/config_setup.py | 395 ++++++++++++++-------------- src/iSponsorBlockTV/constants.py | 2 - src/iSponsorBlockTV/helpers.py | 1 + src/iSponsorBlockTV/setup_wizard.py | 28 ++ 6 files changed, 234 insertions(+), 200 deletions(-) diff --git a/config.json.template b/config.json.template index e2d9e65..42cf626 100644 --- a/config.json.template +++ b/config.json.template @@ -18,5 +18,6 @@ {"id": "", "name": "" } - ] + ], + "api_server": "https://sponsor.ajay.app" } diff --git a/src/iSponsorBlockTV/api_helpers.py b/src/iSponsorBlockTV/api_helpers.py index 8cfe3e8..0d07379 100644 --- a/src/iSponsorBlockTV/api_helpers.py +++ b/src/iSponsorBlockTV/api_helpers.py @@ -27,6 +27,7 @@ def __init__(self, config, web_session: ClientSession) -> None: self.skip_count_tracking = config.skip_count_tracking self.web_session = web_session self.num_devices = len(config.devices) + self.api_server = config.api_server # Not used anymore, maybe it can stay here a little longer @AsyncLRU(maxsize=10) @@ -130,7 +131,7 @@ async def get_segments(self, vid_id): "service": constants.SponsorBlock_service, } 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( url, headers=headers, params=params ) as response: @@ -201,7 +202,7 @@ async def mark_viewed_segments(self, uuids): Lets the contributor know that someone skipped the segment (thanks)""" if self.skip_count_tracking: for i in uuids: - url = constants.SponsorBlock_api + "viewedVideoSponsorTime/" + url = self.api_server + "/api/viewedVideoSponsorTime/" params = {"UUID": i} await self.web_session.post(url, params=params) diff --git a/src/iSponsorBlockTV/config_setup.py b/src/iSponsorBlockTV/config_setup.py index a46015e..8f18f80 100644 --- a/src/iSponsorBlockTV/config_setup.py +++ b/src/iSponsorBlockTV/config_setup.py @@ -1,195 +1,200 @@ -import asyncio - -import aiohttp - -from . import api_helpers, ytlounge - -# Constants for user input prompts -ATVS_REMOVAL_PROMPT = ( - "Do you want to remove the legacy 'atvs' entry (the app won't start" - " with it present)? (y/N) " -) -PAIRING_CODE_PROMPT = "Enter pairing code (found in Settings - Link with TV code): " -ADD_MORE_DEVICES_PROMPT = "Paired with {num_devices} Device(s). Add more? (y/N) " -CHANGE_API_KEY_PROMPT = "API key already specified. Change it? (y/N) " -ADD_API_KEY_PROMPT = ( - "API key only needed for the channel whitelist function. Add it? (y/N) " -) -ENTER_API_KEY_PROMPT = "Enter your API key: " -CHANGE_SKIP_CATEGORIES_PROMPT = "Skip categories already specified. Change them? (y/N) " -ENTER_SKIP_CATEGORIES_PROMPT = ( - "Enter skip categories (space or comma sepparated) Options: [sponsor," - " selfpromo, exclusive_access, interaction, poi_highlight, intro, outro," - " preview, filler, music_offtopic]:\n" -) -WHITELIST_CHANNELS_PROMPT = ( - "Do you want to whitelist any channels from being ad-blocked? (y/N) " -) -SEARCH_CHANNEL_PROMPT = 'Enter a channel name or "/exit" to exit: ' -SELECT_CHANNEL_PROMPT = "Select one option of the above [0-6]: " -ENTER_CHANNEL_ID_PROMPT = "Enter a channel ID: " -ENTER_CUSTOM_CHANNEL_NAME_PROMPT = "Enter the channel name: " -REPORT_SKIPPED_SEGMENTS_PROMPT = ( - "Do you want to report skipped segments to sponsorblock. Only the segment" - " UUID will be sent? (Y/n) " -) -MUTE_ADS_PROMPT = "Do you want to mute native YouTube ads automatically? (y/N) " -SKIP_ADS_PROMPT = "Do you want to skip native YouTube ads automatically? (y/N) " -AUTOPLAY_PROMPT = "Do you want to enable autoplay? (Y/n) " - - -def get_yn_input(prompt): - while choice := input(prompt): - if choice.lower() in ["y", "n"]: - return choice.lower() - print("Invalid input. Please enter 'y' or 'n'.") - - -async def pair_device(): - try: - lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV") - pairing_code = input(PAIRING_CODE_PROMPT) - pairing_code = int( - pairing_code.replace("-", "").replace(" ", "") - ) # remove dashes and spaces - print("Pairing...") - paired = await lounge_controller.pair(pairing_code) - if not paired: - print("Failed to pair device") - return - device = { - "screen_id": lounge_controller.auth.screen_id, - "name": lounge_controller.screen_name, - } - print(f"Paired device: {device['name']}") - return device - except Exception as e: - print(f"Failed to pair device: {e}") - return - - -def main(config, debug: bool) -> None: - print("Welcome to the iSponsorBlockTV cli setup wizard") - loop = asyncio.get_event_loop_policy().get_event_loop() - web_session = aiohttp.ClientSession() - if debug: - loop.set_debug(True) - asyncio.set_event_loop(loop) - if hasattr(config, "atvs"): - print( - "The atvs config option is deprecated and has stopped working. Please read" - " this for more information on how to upgrade to V2:" - " \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2" - ) - choice = get_yn_input(ATVS_REMOVAL_PROMPT) - if choice == "y": - del config["atvs"] - - devices = config.devices - choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices))) - while choice == "y": - task = loop.create_task(pair_device()) - loop.run_until_complete(task) - device = task.result() - if device: - devices.append(device) - choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices))) - config.devices = devices - - apikey = config.apikey - if apikey: - choice = get_yn_input(CHANGE_API_KEY_PROMPT) - if choice == "y": - apikey = input(ENTER_API_KEY_PROMPT) - else: - choice = get_yn_input(ADD_API_KEY_PROMPT) - if choice == "y": - print( - "Get youtube apikey here:" - " https://developers.google.com/youtube/registering_an_application" - ) - apikey = input(ENTER_API_KEY_PROMPT) - config.apikey = apikey - - skip_categories = config.skip_categories - if skip_categories: - choice = get_yn_input(CHANGE_SKIP_CATEGORIES_PROMPT) - if choice == "y": - categories = input(ENTER_SKIP_CATEGORIES_PROMPT) - skip_categories = categories.replace(",", " ").split(" ") - skip_categories = [ - x for x in skip_categories if x != "" - ] # Remove empty strings - else: - categories = input(ENTER_SKIP_CATEGORIES_PROMPT) - skip_categories = categories.replace(",", " ").split(" ") - skip_categories = [ - x for x in skip_categories if x != "" - ] # Remove empty strings - config.skip_categories = skip_categories - - channel_whitelist = config.channel_whitelist - choice = get_yn_input(WHITELIST_CHANNELS_PROMPT) - if choice == "y": - if not apikey: - print( - "WARNING: You need to specify an API key to use this function," - " otherwise the program will fail to start.\nYou can add one by" - " re-running this setup wizard." - ) - api_helper = api_helpers.ApiHelper(config, web_session) - while True: - channel_info = {} - channel = input(SEARCH_CHANNEL_PROMPT) - if channel == "/exit": - break - - task = loop.create_task( - api_helper.search_channels(channel, apikey, web_session) - ) - loop.run_until_complete(task) - results = task.result() - if len(results) == 0: - print("No channels found") - continue - - for i, item in enumerate(results): - print(f"{i}: {item[1]} - Subs: {item[2]}") - print("5: Enter a custom channel ID") - print("6: Go back") - - while choice := input(SELECT_CHANNEL_PROMPT): - if choice in [str(x) for x in range(7)]: - break - print("Invalid choice") - - if choice == "5": - channel_info["id"] = input(ENTER_CHANNEL_ID_PROMPT) - channel_info["name"] = input(ENTER_CUSTOM_CHANNEL_NAME_PROMPT) - channel_whitelist.append(channel_info) - continue - if choice == "6": - continue - - channel_info["id"] = results[int(choice)][0] - channel_info["name"] = results[int(choice)][1] - channel_whitelist.append(channel_info) - # Close web session asynchronously - - config.channel_whitelist = channel_whitelist - - choice = get_yn_input(REPORT_SKIPPED_SEGMENTS_PROMPT) - config.skip_count_tracking = choice != "n" - - choice = get_yn_input(MUTE_ADS_PROMPT) - config.mute_ads = choice == "y" - - choice = get_yn_input(SKIP_ADS_PROMPT) - config.skip_ads = choice == "y" - - choice = get_yn_input(AUTOPLAY_PROMPT) - config.auto_play = choice != "n" - - print("Config finished") - config.save() - loop.run_until_complete(web_session.close()) +import asyncio + +import aiohttp + +from . import api_helpers, ytlounge + +# Constants for user input prompts +ATVS_REMOVAL_PROMPT = ( + "Do you want to remove the legacy 'atvs' entry (the app won't start" + " with it present)? (y/N) " +) +PAIRING_CODE_PROMPT = "Enter pairing code (found in Settings - Link with TV code): " +ADD_MORE_DEVICES_PROMPT = "Paired with {num_devices} Device(s). Add more? (y/N) " +CHANGE_API_KEY_PROMPT = "API key already specified. Change it? (y/N) " +ADD_API_KEY_PROMPT = ( + "API key only needed for the channel whitelist function. Add it? (y/N) " +) +ENTER_API_KEY_PROMPT = "Enter your API key: " +CHANGE_SKIP_CATEGORIES_PROMPT = "Skip categories already specified. Change them? (y/N) " +ENTER_SKIP_CATEGORIES_PROMPT = ( + "Enter skip categories (space or comma sepparated) Options: [sponsor," + " selfpromo, exclusive_access, interaction, poi_highlight, intro, outro," + " preview, filler, music_offtopic]:\n" +) +WHITELIST_CHANNELS_PROMPT = ( + "Do you want to whitelist any channels from being ad-blocked? (y/N) " +) +SEARCH_CHANNEL_PROMPT = 'Enter a channel name or "/exit" to exit: ' +SELECT_CHANNEL_PROMPT = "Select one option of the above [0-6]: " +ENTER_CHANNEL_ID_PROMPT = "Enter a channel ID: " +ENTER_CUSTOM_CHANNEL_NAME_PROMPT = "Enter the channel name: " +REPORT_SKIPPED_SEGMENTS_PROMPT = ( + "Do you want to report skipped segments to sponsorblock. Only the segment" + " UUID will be sent? (Y/n) " +) +MUTE_ADS_PROMPT = "Do you want to mute native YouTube ads automatically? (y/N) " +SKIP_ADS_PROMPT = "Do you want to skip native YouTube ads automatically? (y/N) " +AUTOPLAY_PROMPT = "Do you want to enable autoplay? (Y/n) " +ENTER_API_SERVER_PROMPT = "Enter the custom API server URL (leave blank to use default): " + + +def get_yn_input(prompt): + while choice := input(prompt): + if choice.lower() in ["y", "n"]: + return choice.lower() + print("Invalid input. Please enter 'y' or 'n'.") + + +async def pair_device(): + try: + lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV") + pairing_code = input(PAIRING_CODE_PROMPT) + pairing_code = int( + pairing_code.replace("-", "").replace(" ", "") + ) # remove dashes and spaces + print("Pairing...") + paired = await lounge_controller.pair(pairing_code) + if not paired: + print("Failed to pair device") + return + device = { + "screen_id": lounge_controller.auth.screen_id, + "name": lounge_controller.screen_name, + } + print(f"Paired device: {device['name']}") + return device + except Exception as e: + print(f"Failed to pair device: {e}") + return + + +def main(config, debug: bool) -> None: + print("Welcome to the iSponsorBlockTV cli setup wizard") + loop = asyncio.get_event_loop_policy().get_event_loop() + web_session = aiohttp.ClientSession() + if debug: + loop.set_debug(True) + asyncio.set_event_loop(loop) + if hasattr(config, "atvs"): + print( + "The atvs config option is deprecated and has stopped working. Please read" + " this for more information on how to upgrade to V2:" + " \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2" + ) + choice = get_yn_input(ATVS_REMOVAL_PROMPT) + if choice == "y": + del config["atvs"] + + devices = config.devices + choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices))) + while choice == "y": + task = loop.create_task(pair_device()) + loop.run_until_complete(task) + device = task.result() + if device: + devices.append(device) + choice = get_yn_input(ADD_MORE_DEVICES_PROMPT.format(num_devices=len(devices))) + config.devices = devices + + apikey = config.apikey + if apikey: + choice = get_yn_input(CHANGE_API_KEY_PROMPT) + if choice == "y": + apikey = input(ENTER_API_KEY_PROMPT) + else: + choice = get_yn_input(ADD_API_KEY_PROMPT) + if choice == "y": + print( + "Get youtube apikey here:" + " https://developers.google.com/youtube/registering_an_application" + ) + apikey = input(ENTER_API_KEY_PROMPT) + config.apikey = apikey + + skip_categories = config.skip_categories + if skip_categories: + choice = get_yn_input(CHANGE_SKIP_CATEGORIES_PROMPT) + if choice == "y": + categories = input(ENTER_SKIP_CATEGORIES_PROMPT) + skip_categories = categories.replace(",", " ").split(" ") + skip_categories = [ + x for x in skip_categories if x != "" + ] # Remove empty strings + else: + categories = input(ENTER_SKIP_CATEGORIES_PROMPT) + skip_categories = categories.replace(",", " ").split(" ") + skip_categories = [ + x for x in skip_categories if x != "" + ] # Remove empty strings + config.skip_categories = skip_categories + + channel_whitelist = config.channel_whitelist + choice = get_yn_input(WHITELIST_CHANNELS_PROMPT) + if choice == "y": + if not apikey: + print( + "WARNING: You need to specify an API key to use this function," + " otherwise the program will fail to start.\nYou can add one by" + " re-running this setup wizard." + ) + api_helper = api_helpers.ApiHelper(config, web_session) + while True: + channel_info = {} + channel = input(SEARCH_CHANNEL_PROMPT) + if channel == "/exit": + break + + task = loop.create_task( + api_helper.search_channels(channel, apikey, web_session) + ) + loop.run_until_complete(task) + results = task.result() + if len(results) == 0: + print("No channels found") + continue + + for i, item in enumerate(results): + print(f"{i}: {item[1]} - Subs: {item[2]}") + print("5: Enter a custom channel ID") + print("6: Go back") + + while choice := input(SELECT_CHANNEL_PROMPT): + if choice in [str(x) for x in range(7)]: + break + print("Invalid choice") + + if choice == "5": + channel_info["id"] = input(ENTER_CHANNEL_ID_PROMPT) + channel_info["name"] = input(ENTER_CUSTOM_CHANNEL_NAME_PROMPT) + channel_whitelist.append(channel_info) + continue + if choice == "6": + continue + + channel_info["id"] = results[int(choice)][0] + channel_info["name"] = results[int(choice)][1] + channel_whitelist.append(channel_info) + # Close web session asynchronously + + config.channel_whitelist = channel_whitelist + + choice = get_yn_input(REPORT_SKIPPED_SEGMENTS_PROMPT) + config.skip_count_tracking = choice != "n" + + choice = get_yn_input(MUTE_ADS_PROMPT) + config.mute_ads = choice == "y" + + choice = get_yn_input(SKIP_ADS_PROMPT) + config.skip_ads = choice == "y" + + choice = get_yn_input(AUTOPLAY_PROMPT) + config.auto_play = choice != "n" + + api_server = input(ENTER_API_SERVER_PROMPT) + if api_server: + config.api_server = api_server + + print("Config finished") + config.save() + loop.run_until_complete(web_session.close()) diff --git a/src/iSponsorBlockTV/constants.py b/src/iSponsorBlockTV/constants.py index daaf36e..c376944 100644 --- a/src/iSponsorBlockTV/constants.py +++ b/src/iSponsorBlockTV/constants.py @@ -2,7 +2,6 @@ SponsorBlock_service = "youtube" SponsorBlock_actiontype = "skip" -SponsorBlock_api = "https://sponsor.ajay.app/api/" Youtube_api = "https://www.googleapis.com/youtube/v3/" skip_categories = ( @@ -20,5 +19,4 @@ youtube_client_blacklist = ["TVHTML5_FOR_KIDS"] - config_file_blacklist_keys = ["config_file", "data_dir"] diff --git a/src/iSponsorBlockTV/helpers.py b/src/iSponsorBlockTV/helpers.py index 6b4880e..ef143ea 100644 --- a/src/iSponsorBlockTV/helpers.py +++ b/src/iSponsorBlockTV/helpers.py @@ -42,6 +42,7 @@ def __init__(self, data_dir): self.mute_ads = False self.skip_ads = False self.auto_play = True + self.api_server = "https://sponsor.ajay.app" self.__load() def validate(self): diff --git a/src/iSponsorBlockTV/setup_wizard.py b/src/iSponsorBlockTV/setup_wizard.py index 669c581..bb10834 100644 --- a/src/iSponsorBlockTV/setup_wizard.py +++ b/src/iSponsorBlockTV/setup_wizard.py @@ -876,6 +876,31 @@ def changed_skip(self, event: Checkbox.Changed): 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): """Making this a separate screen to avoid a bug: https://github.com/Textualize/textual/issues/3221""" @@ -915,6 +940,9 @@ def compose(self) -> ComposeResult: yield AutoPlayManager( config=self.config, id="autoplay-manager", classes="container" ) + yield ApiServerManager( + config=self.config, id="api-server-manager", classes="container" + ) def on_mount(self) -> None: if self.check_for_old_config_entries(): From 4e00c62af1682ceb78243bb70bfa4209cec305f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:25:32 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/iSponsorBlockTV/config_setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/iSponsorBlockTV/config_setup.py b/src/iSponsorBlockTV/config_setup.py index 8f18f80..2f6fc79 100644 --- a/src/iSponsorBlockTV/config_setup.py +++ b/src/iSponsorBlockTV/config_setup.py @@ -36,7 +36,9 @@ MUTE_ADS_PROMPT = "Do you want to mute native YouTube ads automatically? (y/N) " SKIP_ADS_PROMPT = "Do you want to skip native YouTube ads automatically? (y/N) " AUTOPLAY_PROMPT = "Do you want to enable autoplay? (Y/n) " -ENTER_API_SERVER_PROMPT = "Enter the custom API server URL (leave blank to use default): " +ENTER_API_SERVER_PROMPT = ( + "Enter the custom API server URL (leave blank to use default): " +) def get_yn_input(prompt):