From ce95b6dbf0e7c9423a8683e8ca3c46f9827e9470 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Sat, 27 Apr 2024 19:17:52 +0200 Subject: [PATCH 1/6] Update dependencies --- requirements.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 293b2ae..a090787 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -aiohttp==3.9.0 +aiohttp==3.9.5 appdirs==1.4.4 argparse==1.4.0 async-cache==1.1.1 -pyytlounge==1.6.3 -rich==13.6.0 +pyytlounge==2.0.0 +rich==13.7.1 ssdp==1.3.0 -textual==0.40.0 +textual==0.58.0 textual-slider==0.1.1 -xmltodict==0.13.0 +xmltodict==0.13.0 \ No newline at end of file From 582b9bf7254d5b7d9ee85b32fc598965bc7a7650 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Sat, 27 Apr 2024 19:26:55 +0200 Subject: [PATCH 2/6] Patch the main aiohttp.ClientSession() into YTlounge --- src/iSponsorBlockTV/config_setup.py | 42 ++++++++++++++++------------- src/iSponsorBlockTV/main.py | 9 ++++--- src/iSponsorBlockTV/setup_wizard.py | 4 +-- src/iSponsorBlockTV/ytlounge.py | 33 +++++++++++++++-------- 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/iSponsorBlockTV/config_setup.py b/src/iSponsorBlockTV/config_setup.py index 51b0138..92a7dbb 100644 --- a/src/iSponsorBlockTV/config_setup.py +++ b/src/iSponsorBlockTV/config_setup.py @@ -5,9 +5,10 @@ import aiohttp from . import api_helpers, ytlounge -async def pair_device(): +async def pair_device(web_session): try: - lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV") + lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV", + web_session=web_session) pairing_code = input( "Enter pairing code (found in Settings - Link with TV code): " ) @@ -33,6 +34,7 @@ async def pair_device(): 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) @@ -43,16 +45,17 @@ def main(config, debug: bool) -> None: " \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2" ) if ( - input( - "Do you want to remove the legacy 'atvs' entry (the app won't start" - " with it present)? (y/n) " - ) - == "y" + input( + "Do you want to remove the legacy 'atvs' entry (the app won't start" + " with it present)? (y/n) " + ) + == "y" ): del config["atvs"] devices = config.devices - while not input(f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n": - task = loop.create_task(pair_device()) + while not input( + f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n": + task = loop.create_task(pair_device(web_session)) loop.run_until_complete(task) device = task.result() if device: @@ -65,10 +68,10 @@ def main(config, debug: bool) -> None: apikey = input("Enter your API key: ") else: if ( - input( - "API key only needed for the channel whitelist function. Add it? (y/n) " - ) - == "y" + input( + "API key only needed for the channel whitelist function. Add it? (y/n) " + ) + == "y" ): print( "Get youtube apikey here:" @@ -79,7 +82,8 @@ def main(config, debug: bool) -> None: skip_categories = config.skip_categories if skip_categories: - if input("Skip categories already specified. Change them? (y/n) ") == "y": + if input( + "Skip categories already specified. Change them? (y/n) ") == "y": categories = input( "Enter skip categories (space or comma sepparated) Options: [sponsor" " selfpromo exclusive_access interaction poi_highlight intro outro" @@ -103,8 +107,9 @@ def main(config, debug: bool) -> None: channel_whitelist = config.channel_whitelist if ( - input("Do you want to whitelist any channels from being ad-blocked? (y/n) ") - == "y" + input( + "Do you want to whitelist any channels from being ad-blocked? (y/n) ") + == "y" ): if not apikey: print( @@ -112,7 +117,6 @@ def main(config, debug: bool) -> None: " otherwise the program will fail to start.\nYou can add one by" " re-running this setup wizard." ) - web_session = aiohttp.ClientSession() api_helper = api_helpers.ApiHelper(config, web_session) while True: channel_info = {} @@ -152,7 +156,6 @@ def main(config, debug: bool) -> None: channel_info["name"] = results[int(choice)][1] channel_whitelist.append(channel_info) # Close web session asynchronously - loop.run_until_complete(web_session.close()) config.channel_whitelist = channel_whitelist @@ -161,7 +164,8 @@ def main(config, debug: bool) -> None: "Do you want to report skipped segments to sponsorblock. Only the segment" " UUID will be sent? (y/n) " ) - == "n" + == "n" ) print("Config finished") config.save() + loop.run_until_complete(web_session.close()) diff --git a/src/iSponsorBlockTV/main.py b/src/iSponsorBlockTV/main.py index 8cc6e9d..b624d00 100644 --- a/src/iSponsorBlockTV/main.py +++ b/src/iSponsorBlockTV/main.py @@ -10,13 +10,14 @@ from . import api_helpers, ytlounge class DeviceListener: - def __init__(self, api_helper, config, device, debug: bool): + def __init__(self, api_helper, config, device, debug: bool, web_session): self.task: Optional[asyncio.Task] = None self.api_helper = api_helper self.offset = device.offset self.name = device.name self.cancelled = False self.logger = logging.getLogger(f"iSponsorBlockTV-{device.screen_id}") + self.web_session = web_session if debug: self.logger.setLevel(logging.DEBUG) else: @@ -28,7 +29,7 @@ class DeviceListener: self.logger.addHandler(sh) self.logger.info(f"Starting device") self.lounge_controller = ytlounge.YtLoungeApi( - device.screen_id, config, api_helper, self.logger + device.screen_id, config, api_helper, self.logger, self.web_session ) # Ensures that we have a valid auth token @@ -155,7 +156,7 @@ def main(config, debug): web_session = aiohttp.ClientSession(loop=loop, connector=tcp_connector) api_helper = api_helpers.ApiHelper(config, web_session) for i in config.devices: - device = DeviceListener(api_helper, config, i, debug) + device = DeviceListener(api_helper, config, i, debug, web_session) devices.append(device) tasks.append(loop.create_task(device.loop())) tasks.append(loop.create_task(device.refresh_auth_loop())) @@ -165,3 +166,5 @@ def main(config, debug): 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() diff --git a/src/iSponsorBlockTV/setup_wizard.py b/src/iSponsorBlockTV/setup_wizard.py index e692abe..b111b63 100644 --- a/src/iSponsorBlockTV/setup_wizard.py +++ b/src/iSponsorBlockTV/setup_wizard.py @@ -234,7 +234,7 @@ class AddDevice(ModalWithClickExit): def __init__(self, config, **kwargs) -> None: super().__init__(**kwargs) self.config = config - web_session = aiohttp.ClientSession() + self.web_session = aiohttp.ClientSession() self.api_helper = api_helpers.ApiHelper(config, web_session) self.devices_discovered_dial = [] @@ -336,7 +336,7 @@ class AddDevice(ModalWithClickExit): @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") + lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV", web_session=self.web_session) pairing_code = self.query_one("#pairing-code-input").value pairing_code = int( pairing_code.replace("-", "").replace(" ", "") diff --git a/src/iSponsorBlockTV/ytlounge.py b/src/iSponsorBlockTV/ytlounge.py index 18087f7..2cc2a1d 100644 --- a/src/iSponsorBlockTV/ytlounge.py +++ b/src/iSponsorBlockTV/ytlounge.py @@ -1,3 +1,4 @@ +from aiohttp import ClientSession import asyncio import json @@ -9,8 +10,13 @@ create_task = asyncio.create_task class YtLoungeApi(pyytlounge.YtLoungeApi): - def __init__(self, screen_id, config=None, api_helper=None, logger=None): + def __init__(self, screen_id, config=None, api_helper=None, logger=None, + web_session: ClientSession = None): super().__init__("iSponsorBlockTV", logger=logger) + if web_session is not None: + asyncio.get_event_loop().run_until_complete( + self.close()) # Close the default connection + self.session = web_session # And use the one we passed self.auth.screen_id = screen_id self.auth.lounge_id_token = None self.api_helper = api_helper @@ -75,13 +81,13 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): self.logger.info("Ad has ended, unmuting") create_task(self.mute(False, override=True)) elif ( - self.skip_ads and data["isSkipEnabled"] == "true" + 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 + 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)) @@ -92,7 +98,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): # Gets segments for the next video before it starts playing elif event_type == "autoplayUpNext": if len(args) > 0 and ( - vid_id := args[0]["videoId"] + 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)) @@ -105,13 +111,13 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): self.logger.info(f"Getting segments for next video: {vid_id}") create_task(self.api_helper.get_segments(vid_id)) elif ( - self.skip_ads and data["isSkipEnabled"] == "true" + 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 + 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)) @@ -122,7 +128,8 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): for device in devices: if device["type"] == "LOUNGE_SCREEN": device_info = json.loads(device.get("deviceInfo", "{}")) - if device_info.get("clientName", "") in youtube_client_blacklist: + if device_info.get("clientName", + "") in youtube_client_blacklist: self._sid = None self._gsession = None # Force disconnect @@ -134,7 +141,8 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): create_task(self.play_video(video_id_saved)) elif event_type == "loungeScreenDisconnected": data = args[0] - if data["reason"] == "disconnectedByUserScreenInitiated": # Short playing? + if data[ + "reason"] == "disconnectedByUserScreenInitiated": # Short playing? self.shorts_disconnected = True super()._process_event(event_id, event_type, args) @@ -152,17 +160,20 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): 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( "setVolume", - {"volume": self.volume_state.get("volume", 100), "muted": mute_str}, + {"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"} + "setAutoplayMode", + {"autoplayMode": "ENABLED" if enabled else "DISABLED"} ) async def play_video(self, video_id: str) -> bool: From 213ae97bf2a00154da7d59c6b5ce75bfd805e0e2 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Sun, 5 May 2024 19:07:03 +0200 Subject: [PATCH 3/6] Fix web_session --- src/iSponsorBlockTV/setup_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iSponsorBlockTV/setup_wizard.py b/src/iSponsorBlockTV/setup_wizard.py index b111b63..4411686 100644 --- a/src/iSponsorBlockTV/setup_wizard.py +++ b/src/iSponsorBlockTV/setup_wizard.py @@ -235,7 +235,7 @@ class AddDevice(ModalWithClickExit): super().__init__(**kwargs) self.config = config self.web_session = aiohttp.ClientSession() - self.api_helper = api_helpers.ApiHelper(config, web_session) + self.api_helper = api_helpers.ApiHelper(config, self.web_session) self.devices_discovered_dial = [] def compose(self) -> ComposeResult: From dd42e20dc48c40e9ad9745bbae25cb67ab7c70eb Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Sun, 5 May 2024 19:16:27 +0200 Subject: [PATCH 4/6] Remove session closer, it seems to break the config setup --- src/iSponsorBlockTV/ytlounge.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/iSponsorBlockTV/ytlounge.py b/src/iSponsorBlockTV/ytlounge.py index 2cc2a1d..56c7490 100644 --- a/src/iSponsorBlockTV/ytlounge.py +++ b/src/iSponsorBlockTV/ytlounge.py @@ -14,8 +14,6 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): web_session: ClientSession = None): super().__init__("iSponsorBlockTV", logger=logger) if web_session is not None: - asyncio.get_event_loop().run_until_complete( - self.close()) # Close the default connection self.session = web_session # And use the one we passed self.auth.screen_id = screen_id self.auth.lounge_id_token = None From d21bb6320f0c93940ebd4e3046b65e345216c4e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 21:31:56 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- requirements.txt | 2 +- src/iSponsorBlockTV/config_setup.py | 36 +++++++++++++--------------- src/iSponsorBlockTV/setup_wizard.py | 4 +++- src/iSponsorBlockTV/ytlounge.py | 37 +++++++++++++++-------------- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/requirements.txt b/requirements.txt index a090787..e30c014 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ rich==13.7.1 ssdp==1.3.0 textual==0.58.0 textual-slider==0.1.1 -xmltodict==0.13.0 \ No newline at end of file +xmltodict==0.13.0 diff --git a/src/iSponsorBlockTV/config_setup.py b/src/iSponsorBlockTV/config_setup.py index 92a7dbb..3e3bb95 100644 --- a/src/iSponsorBlockTV/config_setup.py +++ b/src/iSponsorBlockTV/config_setup.py @@ -7,8 +7,9 @@ from . import api_helpers, ytlounge async def pair_device(web_session): try: - lounge_controller = ytlounge.YtLoungeApi("iSponsorBlockTV", - web_session=web_session) + lounge_controller = ytlounge.YtLoungeApi( + "iSponsorBlockTV", web_session=web_session + ) pairing_code = input( "Enter pairing code (found in Settings - Link with TV code): " ) @@ -45,16 +46,15 @@ def main(config, debug: bool) -> None: " \nhttps://github.com/dmunozv04/iSponsorBlockTV/wiki/Migrate-from-V1-to-V2" ) if ( - input( - "Do you want to remove the legacy 'atvs' entry (the app won't start" - " with it present)? (y/n) " - ) - == "y" + input( + "Do you want to remove the legacy 'atvs' entry (the app won't start" + " with it present)? (y/n) " + ) + == "y" ): del config["atvs"] devices = config.devices - while not input( - f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n": + while not input(f"Paired with {len(devices)} Device(s). Add more? (y/n) ") == "n": task = loop.create_task(pair_device(web_session)) loop.run_until_complete(task) device = task.result() @@ -68,10 +68,10 @@ def main(config, debug: bool) -> None: apikey = input("Enter your API key: ") else: if ( - input( - "API key only needed for the channel whitelist function. Add it? (y/n) " - ) - == "y" + input( + "API key only needed for the channel whitelist function. Add it? (y/n) " + ) + == "y" ): print( "Get youtube apikey here:" @@ -82,8 +82,7 @@ def main(config, debug: bool) -> None: skip_categories = config.skip_categories if skip_categories: - if input( - "Skip categories already specified. Change them? (y/n) ") == "y": + if input("Skip categories already specified. Change them? (y/n) ") == "y": categories = input( "Enter skip categories (space or comma sepparated) Options: [sponsor" " selfpromo exclusive_access interaction poi_highlight intro outro" @@ -107,9 +106,8 @@ def main(config, debug: bool) -> None: channel_whitelist = config.channel_whitelist if ( - input( - "Do you want to whitelist any channels from being ad-blocked? (y/n) ") - == "y" + input("Do you want to whitelist any channels from being ad-blocked? (y/n) ") + == "y" ): if not apikey: print( @@ -164,7 +162,7 @@ def main(config, debug: bool) -> None: "Do you want to report skipped segments to sponsorblock. Only the segment" " UUID will be sent? (y/n) " ) - == "n" + == "n" ) print("Config finished") config.save() diff --git a/src/iSponsorBlockTV/setup_wizard.py b/src/iSponsorBlockTV/setup_wizard.py index 4411686..da4d66f 100644 --- a/src/iSponsorBlockTV/setup_wizard.py +++ b/src/iSponsorBlockTV/setup_wizard.py @@ -336,7 +336,9 @@ class AddDevice(ModalWithClickExit): @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) + lounge_controller = ytlounge.YtLoungeApi( + "iSponsorBlockTV", web_session=self.web_session + ) pairing_code = self.query_one("#pairing-code-input").value pairing_code = int( pairing_code.replace("-", "").replace(" ", "") diff --git a/src/iSponsorBlockTV/ytlounge.py b/src/iSponsorBlockTV/ytlounge.py index 56c7490..625329f 100644 --- a/src/iSponsorBlockTV/ytlounge.py +++ b/src/iSponsorBlockTV/ytlounge.py @@ -1,8 +1,8 @@ -from aiohttp import ClientSession import asyncio import json import pyytlounge +from aiohttp import ClientSession from .constants import youtube_client_blacklist @@ -10,8 +10,14 @@ create_task = asyncio.create_task class YtLoungeApi(pyytlounge.YtLoungeApi): - def __init__(self, screen_id, config=None, api_helper=None, logger=None, - web_session: ClientSession = None): + def __init__( + self, + screen_id, + 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 @@ -79,13 +85,13 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): self.logger.info("Ad has ended, unmuting") create_task(self.mute(False, override=True)) elif ( - self.skip_ads and data["isSkipEnabled"] == "true" + 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 + 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)) @@ -96,7 +102,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): # Gets segments for the next video before it starts playing elif event_type == "autoplayUpNext": if len(args) > 0 and ( - vid_id := args[0]["videoId"] + 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)) @@ -109,13 +115,13 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): self.logger.info(f"Getting segments for next video: {vid_id}") create_task(self.api_helper.get_segments(vid_id)) elif ( - self.skip_ads and data["isSkipEnabled"] == "true" + 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 + 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)) @@ -126,8 +132,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): for device in devices: if device["type"] == "LOUNGE_SCREEN": device_info = json.loads(device.get("deviceInfo", "{}")) - if device_info.get("clientName", - "") in youtube_client_blacklist: + if device_info.get("clientName", "") in youtube_client_blacklist: self._sid = None self._gsession = None # Force disconnect @@ -139,8 +144,7 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): create_task(self.play_video(video_id_saved)) elif event_type == "loungeScreenDisconnected": data = args[0] - if data[ - "reason"] == "disconnectedByUserScreenInitiated": # Short playing? + if data["reason"] == "disconnectedByUserScreenInitiated": # Short playing? self.shorts_disconnected = True super()._process_event(event_id, event_type, args) @@ -158,20 +162,17 @@ class YtLoungeApi(pyytlounge.YtLoungeApi): 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( "setVolume", - {"volume": self.volume_state.get("volume", 100), - "muted": mute_str}, + {"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"} + "setAutoplayMode", {"autoplayMode": "ENABLED" if enabled else "DISABLED"} ) async def play_video(self, video_id: str) -> bool: From 1ab7e73b521acf3919816765031bb5ad85db97f3 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Wed, 29 May 2024 23:41:42 +0200 Subject: [PATCH 6/6] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index de6e83e..c497a20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "iSponsorBlockTV" -version = "2.0.6" +version = "2.0.7" authors = [ {"name" = "dmunozv04"} ]