From cdd292538353ae75b363e2c87f007c69c2e68070 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 09:53:28 -0800 Subject: [PATCH 01/12] Updated PTSL_VERSION --- ptsl/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptsl/client.py b/ptsl/client.py index 127fc89..78d3033 100644 --- a/ptsl/client.py +++ b/ptsl/client.py @@ -19,7 +19,7 @@ from ptsl.ops import Operation -PTSL_VERSION = 3 +PTSL_VERSION = 5 @contextmanager From fc2285a6c418334e8969918ed444f11cad083074 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 09:55:23 -0800 Subject: [PATCH 02/12] Added `ClearAllMemoryLocations` command --- ptsl/ops/memory_locations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ptsl/ops/memory_locations.py b/ptsl/ops/memory_locations.py index e34e7fb..5493391 100644 --- a/ptsl/ops/memory_locations.py +++ b/ptsl/ops/memory_locations.py @@ -11,3 +11,6 @@ class GetMemoryLocations(Operation): class CreateMemoryLocation(Operation): pass + +class ClearAllMemoryLocations(Operation): + pass From 9b0e524e76eb39797594d42e1f7b1bdf4a96fb53 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 10:13:25 -0800 Subject: [PATCH 03/12] Removed extraneous types (they were already added) --- ptsl/ops/memory_locations.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ptsl/ops/memory_locations.py b/ptsl/ops/memory_locations.py index 5493391..e34e7fb 100644 --- a/ptsl/ops/memory_locations.py +++ b/ptsl/ops/memory_locations.py @@ -11,6 +11,3 @@ class GetMemoryLocations(Operation): class CreateMemoryLocation(Operation): pass - -class ClearAllMemoryLocations(Operation): - pass From 70357cb055c3182dea1ce5197514dddaa4d80707 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 10:13:44 -0800 Subject: [PATCH 04/12] Added a JSON request-response function --- ptsl/client.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/ptsl/client.py b/ptsl/client.py index 78d3033..b106404 100644 --- a/ptsl/client.py +++ b/ptsl/client.py @@ -16,7 +16,7 @@ from ptsl import PTSL_pb2_grpc from ptsl import PTSL_pb2 as pt from ptsl.errors import CommandError -from ptsl.ops import Operation +from ptsl.ops import Operation, operation PTSL_VERSION = 5 @@ -137,6 +137,37 @@ def __init__(self, raise grpc_error + def run_command(self, command_id: pt.CommandId, request: dict) -> Optional[dict]: + """ + Run a command on the client with a JSON request. + + :param command_id: The command to run + :param request: The request parameters. This dict will be converted to + JSON. + :returns: The response if any. This is the JSON response returned by + the server and converted to a dict. + """ + request_body_json = json.dumps(request) + response = self._send_sync_request(command_id, request_body_json) + + if response.header.status == pt.Failed: + cleaned_response_error_json = response.response_error_json + # self._response_error_json_cleanup( + # response.response_error_json) + command_errors = json_format.Parse(cleaned_response_error_json, + pt.ResponseError()) + raise CommandError(list(command_errors.errors)) + + elif response.header.status == pt.Completed: + return json.loads(response.response_body_json) + else: + # FIXME: dump out for now, will be on the lookout for when + # this happens + assert False, \ + f"Unexpected response code {response.header.status} " + \ + f"({pt.TaskStatus.Name(response.header.status)})" + + def run(self, operation: Operation) -> None: """ Run an operation on the client. From 4c61c47c18756bff6bc85a11f47c214d1d1a227c Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 11:53:14 -0800 Subject: [PATCH 05/12] Added a new example, a shell that runs commands --- examples/toolshell.py | 126 ++++++++++++++++++++++++++++++++++++++++++ ptsl/client.py | 5 +- 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 examples/toolshell.py diff --git a/examples/toolshell.py b/examples/toolshell.py new file mode 100644 index 0000000..2f1503a --- /dev/null +++ b/examples/toolshell.py @@ -0,0 +1,126 @@ +# toolshell.py + +import cmd +import shlex +from typing import Optional + +import ptsl +from ptsl import PTSL_pb2 as pt + +class ToolShell(cmd.Cmd): + intro = """ +Toolshell is a demonstration command interpreter that +can remotely operate Pro Tools. Type `help` or `?` to +list commands. + +To begin, type `connect`. + """ + prompt = "(pt) " + + client = None + + def run_command_on_session(self, command_id: pt.CommandId, args: dict) -> Optional[dict]: + if self.client is None: + print("Command failed, not connected") + return None + + try: + r = self.client.run_command(command_id, args) + return r + except ptsl.errors.CommandError as e: + if e.error_type == pt.PT_NoOpenedSession: + print("command failed, no session is currently open") + return None + except: + print("Command failed, Pro Tools may not be running") + return None + + def do_connect(self, _): + 'Connect to Pro Tools' + self.client = ptsl.client.Client(company_name="py-ptsl", + application_name="Toolshell") + if self.client is not None: + self.prompt = f"(pt/connected) " + + def do_sinfo(self, _): + 'Print info about the open session: SINFO' + r = self.run_command_on_session(pt.GetSessionName, {}) + + assert r, "Failed to receive a response" + session_name = r['session_name'] + r = self.run_command_on_session(pt.GetSessionIDs, {}) + assert r + print(f"Connected to Pro Tools session \"{session_name}\"") + print(f"Session origin ID: {r['origin_id']}") + print(f"Session instance ID: {r['instance_id']}") + + def do_newsession(self, args): + 'Create a new session: NEWSESSION name save-path sample-rate' + name, path, sr = shlex.split(args) + print(f"Creating new session {name} at {path} and SR {sr}") + command_args = {'session_name': name, + 'session_location': path, + 'file_type': 'FT_WAVE', + 'sample_rate': 'SR_' + str(sr), + 'bit_depth': 'Bit24', + 'input_output_settings': "IO_Last", + "is_interleaved": True, + "is_cloud_project": False, + "create_from_template": False, + "template_group": "", + "template_name": "" + } + assert self.client + self.client.run_command(pt.CreateSession, command_args) + + def do_locate(self, args): + 'Locate to a given time in the current main counter format: LOCATE time' + time = args.strip() + command_args = {'play_start_marker_time': time, + 'in_time': time, + 'out_time': time, + } + self.run_command_on_session(pt.SetTimelineSelection, command_args) + + def do_newmemloc(self, args): + 'Create a new marker memory location: NEWMEMLOC start-time' + command_args = {'name': 'New Marker', + 'start_time': args.strip(), + 'end_time': args.strip(), + 'time_properties': 'TP_Marker', + 'reference': 'MLR_FollowTrackTimebase', + 'general_properties': { + 'zoom_settings': False, + 'pre_post_roll_times': False, + 'track_visibility': False, + 'track_heights': False, + 'group_enables': False, + 'window_configuration': False, + }, + 'comments': "Created by toolshell", + 'color_index': 1, + 'location': 'MLC_MainRuler' + } + + self.run_command_on_session(pt.CreateMemoryLocation, command_args) + + def do_play(self, _): + 'Toggle the play state of the transport: PLAY' + assert self.client + try: + self.client.run_command(pt.CommandId.TogglePlayState, {}) + except ptsl.errors.CommandError as e: + if e.error_type == pt.PT_NoOpenedSession: + print("play failed, no session is currently open") + return False + + def do_bye(self, _): + 'Quit Toolshell and return to your shell: BYE' + print("Tooshell quitting...") + if self.client: + self.client.close() + return True + + +if __name__ == '__main__': + ToolShell().cmdloop() diff --git a/ptsl/client.py b/ptsl/client.py index b106404..018b071 100644 --- a/ptsl/client.py +++ b/ptsl/client.py @@ -159,7 +159,10 @@ def run_command(self, command_id: pt.CommandId, request: dict) -> Optional[dict] raise CommandError(list(command_errors.errors)) elif response.header.status == pt.Completed: - return json.loads(response.response_body_json) + if len(response.response_body_json) > 0: + return json.loads(response.response_body_json) + else: + return None else: # FIXME: dump out for now, will be on the lookout for when # this happens From 1827d00ca3db79a9da925a0b829837cf47ea1f3f Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 11:54:12 -0800 Subject: [PATCH 06/12] autopep --- examples/toolshell.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/toolshell.py b/examples/toolshell.py index 2f1503a..95f1cfd 100644 --- a/examples/toolshell.py +++ b/examples/toolshell.py @@ -7,6 +7,7 @@ import ptsl from ptsl import PTSL_pb2 as pt + class ToolShell(cmd.Cmd): intro = """ Toolshell is a demonstration command interpreter that @@ -48,7 +49,7 @@ def do_sinfo(self, _): assert r, "Failed to receive a response" session_name = r['session_name'] - r = self.run_command_on_session(pt.GetSessionIDs, {}) + r = self.run_command_on_session(pt.GetSessionIDs, {}) assert r print(f"Connected to Pro Tools session \"{session_name}\"") print(f"Session origin ID: {r['origin_id']}") @@ -64,12 +65,12 @@ def do_newsession(self, args): 'sample_rate': 'SR_' + str(sr), 'bit_depth': 'Bit24', 'input_output_settings': "IO_Last", - "is_interleaved": True, - "is_cloud_project": False, - "create_from_template": False, - "template_group": "", - "template_name": "" - } + "is_interleaved": True, + "is_cloud_project": False, + "create_from_template": False, + "template_group": "", + "template_name": "" + } assert self.client self.client.run_command(pt.CreateSession, command_args) @@ -96,12 +97,12 @@ def do_newmemloc(self, args): 'track_heights': False, 'group_enables': False, 'window_configuration': False, - }, + }, 'comments': "Created by toolshell", 'color_index': 1, 'location': 'MLC_MainRuler' - } - + } + self.run_command_on_session(pt.CreateMemoryLocation, command_args) def do_play(self, _): From f574ab574f68b416d7ad2dfee94559434a2ee4e7 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 11:57:24 -0800 Subject: [PATCH 07/12] Updated version, some lints --- ptsl/client.py | 1 - pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ptsl/client.py b/ptsl/client.py index 018b071..ef617a4 100644 --- a/ptsl/client.py +++ b/ptsl/client.py @@ -169,7 +169,6 @@ def run_command(self, command_id: pt.CommandId, request: dict) -> Optional[dict] assert False, \ f"Unexpected response code {response.header.status} " + \ f"({pt.TaskStatus.Name(response.header.status)})" - def run(self, operation: Operation) -> None: """ diff --git a/pyproject.toml b/pyproject.toml index c36b18b..a6fd345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [tool.poetry] name = "py-ptsl" -version = "500.0.0" +version = "500.1.0" description = "Native Python PTSL (Pro Tools Scripting Library) RPC interface" authors = ["Jamie Hardt "] license = "BSD-3-Clause" From 00fe81b19b3e0db5313bc22b4508a78528dc3fd5 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 12:04:21 -0800 Subject: [PATCH 08/12] Linting --- ptsl/client.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/ptsl/client.py b/ptsl/client.py index ef617a4..bddeb8f 100644 --- a/ptsl/client.py +++ b/ptsl/client.py @@ -16,7 +16,7 @@ from ptsl import PTSL_pb2_grpc from ptsl import PTSL_pb2 as pt from ptsl.errors import CommandError -from ptsl.ops import Operation, operation +from ptsl.ops import Operation PTSL_VERSION = 5 @@ -137,14 +137,15 @@ def __init__(self, raise grpc_error - def run_command(self, command_id: pt.CommandId, request: dict) -> Optional[dict]: + def run_command(self, command_id: pt.CommandId, + request: dict) -> Optional[dict]: """ Run a command on the client with a JSON request. - :param command_id: The command to run - :param request: The request parameters. This dict will be converted to + :param command_id: The command to run + :param request: The request parameters. This dict will be converted to JSON. - :returns: The response if any. This is the JSON response returned by + :returns: The response if any. This is the JSON response returned by the server and converted to a dict. """ request_body_json = json.dumps(request) @@ -170,20 +171,20 @@ def run_command(self, command_id: pt.CommandId, request: dict) -> Optional[dict] f"Unexpected response code {response.header.status} " + \ f"({pt.TaskStatus.Name(response.header.status)})" - def run(self, operation: Operation) -> None: + def run(self, op: Operation) -> None: """ Run an operation on the client. :raises: `CommandError` if the server returns an error """ - self.auditor.run_called(operation.command_id()) + self.auditor.run_called(op.command_id()) # convert the request body into JSON - request_body_json = self._prepare_operation_request_json(operation) - response = self._send_sync_request(operation.command_id(), + request_body_json = self._prepare_operation_request_json(op) + response = self._send_sync_request(op.command_id(), request_body_json) - operation.status = response.header.status + op.status = response.header.status if response.header.status == pt.Failed: cleaned_response_error_json = response.response_error_json @@ -194,7 +195,7 @@ def run(self, operation: Operation) -> None: raise CommandError(list(command_errors.errors)) elif response.header.status == pt.Completed: - self._handle_completed_response(operation, response) + self._handle_completed_response(op, response) else: # FIXME: dump out for now, will be on the lookout for when # this happens From 57f5cde0a88721fdd5fa9375c14bdb804cfaea11 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 12:06:50 -0800 Subject: [PATCH 09/12] flake8 --- examples/toolshell.py | 13 +++++++------ ptsl/client.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/toolshell.py b/examples/toolshell.py index 95f1cfd..b313f80 100644 --- a/examples/toolshell.py +++ b/examples/toolshell.py @@ -10,8 +10,8 @@ class ToolShell(cmd.Cmd): intro = """ -Toolshell is a demonstration command interpreter that -can remotely operate Pro Tools. Type `help` or `?` to +Toolshell is a demonstration command interpreter that +can remotely operate Pro Tools. Type `help` or `?` to list commands. To begin, type `connect`. @@ -20,7 +20,8 @@ class ToolShell(cmd.Cmd): client = None - def run_command_on_session(self, command_id: pt.CommandId, args: dict) -> Optional[dict]: + def run_command_on_session(self, command_id: pt.CommandId, + args: dict) -> Optional[dict]: if self.client is None: print("Command failed, not connected") return None @@ -32,7 +33,7 @@ def run_command_on_session(self, command_id: pt.CommandId, args: dict) -> Option if e.error_type == pt.PT_NoOpenedSession: print("command failed, no session is currently open") return None - except: + except Exception: print("Command failed, Pro Tools may not be running") return None @@ -41,7 +42,7 @@ def do_connect(self, _): self.client = ptsl.client.Client(company_name="py-ptsl", application_name="Toolshell") if self.client is not None: - self.prompt = f"(pt/connected) " + self.prompt = "(pt/connected) " def do_sinfo(self, _): 'Print info about the open session: SINFO' @@ -75,7 +76,7 @@ def do_newsession(self, args): self.client.run_command(pt.CreateSession, command_args) def do_locate(self, args): - 'Locate to a given time in the current main counter format: LOCATE time' + 'Locate to a given time: LOCATE time' time = args.strip() command_args = {'play_start_marker_time': time, 'in_time': time, diff --git a/ptsl/client.py b/ptsl/client.py index bddeb8f..fd517ed 100644 --- a/ptsl/client.py +++ b/ptsl/client.py @@ -16,7 +16,7 @@ from ptsl import PTSL_pb2_grpc from ptsl import PTSL_pb2 as pt from ptsl.errors import CommandError -from ptsl.ops import Operation +from ptsl.ops import Operation PTSL_VERSION = 5 @@ -137,7 +137,7 @@ def __init__(self, raise grpc_error - def run_command(self, command_id: pt.CommandId, + def run_command(self, command_id: pt.CommandId, request: dict) -> Optional[dict]: """ Run a command on the client with a JSON request. From 6042a93aa6f0a90ae5181baad7c8e9dc9b86740e Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 12:35:09 -0800 Subject: [PATCH 10/12] Added a newtracks command --- examples/toolshell.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/toolshell.py b/examples/toolshell.py index b313f80..2c90fb5 100644 --- a/examples/toolshell.py +++ b/examples/toolshell.py @@ -2,6 +2,7 @@ import cmd import shlex +import os.path from typing import Optional import ptsl @@ -61,7 +62,7 @@ def do_newsession(self, args): name, path, sr = shlex.split(args) print(f"Creating new session {name} at {path} and SR {sr}") command_args = {'session_name': name, - 'session_location': path, + 'session_location': os.path.expanduser(path), 'file_type': 'FT_WAVE', 'sample_rate': 'SR_' + str(sr), 'bit_depth': 'Bit24', @@ -75,6 +76,18 @@ def do_newsession(self, args): assert self.client self.client.run_command(pt.CreateSession, command_args) + def do_newtracks(self, args): + 'Create new audio track: NEWTRACKS count format' + count, fmt = shlex.split(args) + command_args = {'number_of_tracks': count, + 'track_name': "New Track", + 'track_format': 'TF_' + fmt, + 'track_type': 'TT_Audio', + 'track_timebase': 'TTB_Samples', + 'insertion_point_position': 'TIPoint_Unknown', + } + self.run_command_on_session(pt.CreateNewTracks, command_args) + def do_locate(self, args): 'Locate to a given time: LOCATE time' time = args.strip() From 4e62b1b12ff980ca5aa46685989755d68d8d37e9 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 12:49:58 -0800 Subject: [PATCH 11/12] Changed prompt a little --- examples/toolshell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/toolshell.py b/examples/toolshell.py index 2c90fb5..b609f71 100644 --- a/examples/toolshell.py +++ b/examples/toolshell.py @@ -17,7 +17,7 @@ class ToolShell(cmd.Cmd): To begin, type `connect`. """ - prompt = "(pt) " + prompt = "(not connected) " client = None @@ -43,7 +43,7 @@ def do_connect(self, _): self.client = ptsl.client.Client(company_name="py-ptsl", application_name="Toolshell") if self.client is not None: - self.prompt = "(pt/connected) " + self.prompt = "(pt) " def do_sinfo(self, _): 'Print info about the open session: SINFO' From 7ba83ea8d70f361e7d92994558f09572703b0c51 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Fri, 22 Nov 2024 12:50:07 -0800 Subject: [PATCH 12/12] Added toolshell to readme example list --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cfae0e7..e62b4f3 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ client. options to be set from the terminal. - [pt_pasteboard.py](examples/pt_pasteboard.py) - Demonstrates triggering cut/copy/paste actions. +- [toolshell.py](examples/toolshell.py) - Implements a command line + interface for Pro Tools. ### Sending Commands To Pro Tools with the `Engine` class