From 98ca1ec4625446672899f680b6f16c78a4a571f4 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Sun, 24 Nov 2024 16:23:39 -0800 Subject: [PATCH 1/8] Implementing an interactive shell ...for browsing metadata --- wavinfo/__main__.py | 103 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/wavinfo/__main__.py b/wavinfo/__main__.py index a7a4de9..d144d68 100644 --- a/wavinfo/__main__.py +++ b/wavinfo/__main__.py @@ -7,6 +7,9 @@ import sys import json from enum import Enum from base64 import b64encode +from cmd import Cmd +from shlex import split +from typing import List, Dict class MyJSONEncoder(json.JSONEncoder): @@ -23,6 +26,85 @@ class MissingDataError(RuntimeError): pass +class MetaBrowser(Cmd): + prompt = "(wavinfo) " + + metadata: List | Dict + path: List[str] = [] + + @property + def cwd(self): + root: List | Dict = self.metadata + for key in self.path: + if isinstance(root, list): + root = root[int(key)] + else: + root = root[key] + + return root + + @staticmethod + def print_value(collection, key): + val = collection[key] + if isinstance(val, int): + print(f" - {key}: {val}") + elif isinstance(val, str): + print(f" - {key}: \"{val}\"") + elif isinstance(val, dict): + print(f" - {key}: Dict ({len(val)} keys)") + elif isinstance(val, list): + print(f" - {key}: List ({len(val)} keys)") + elif isinstance(val, bytes): + print(f" - {key}: ({len(val)} bytes)") + elif val == None: + print(f" - {key}: (NO VALUE)") + else: + print(f" - {key}: Unknown") + + def do_ls(self, _): + 'List items at the current node: LS' + root = self.cwd + + if isinstance(root, list): + print(f"List:") + for i in range(len(root)): + self.print_value(root, i) + + elif isinstance(root, dict): + print(f"Dictionary:") + for key in root: + self.print_value(root, key) + + else: + print(f"Cannot print node, is not a list or dictionary.") + + def do_cd(self, args): + 'Switch to a different node: CD node-name | ".."' + argv = split(args) + if argv[0] == "..": + self.path = self.path[0:-1] + else: + if isinstance(self.cwd, list): + if int(argv[0]) < len(self.cwd): + self.path = self.path + [argv[0]] + else: + print(f"Index {argv[0]} does not exist") + elif isinstance(self.cwd, dict): + if argv[0] in self.cwd.keys(): + self.path = self.path + [argv[0]] + else: + print(f"Key \"{argv[0]}\" does not exist") + + if len(self.path) > 0: + self.prompt = "(" + "/".join(self.path) + ") " + else: + self.prompt = "(wavinfo) " + + def do_bye(self, _): + 'Exit the interactive browser: BYE' + return True + + def main(): parser = OptionParser() @@ -38,7 +120,15 @@ def main(): default=False, action='store_true') + parser.add_option('-i', + help='Read metadata with an interactive prompt', + default=False, + action='store_true') + (options, args) = parser.parse_args(sys.argv) + + interactive_dict = [] + for arg in args[1:]: try: this_file = WavInfoReader(path=arg) @@ -65,7 +155,12 @@ def main(): ret_dict['scopes'][scope][name] = value - json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2) + if options.i: + interactive_dict.append(ret_dict) + else: + json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, + indent=2) + except MissingDataError as e: print("MissingDataError: Missing metadata (%s) in file %s" % (e, arg), file=sys.stderr) @@ -73,6 +168,12 @@ def main(): except Exception as e: raise e + if len(interactive_dict) > 0: + cli = MetaBrowser() + cli.metadata = interactive_dict + cli.cmdloop() + + if __name__ == "__main__": main() From d560e5a9f08b22cd8275661fe830e9c71dc671ee Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 25 Nov 2024 10:04:50 -0800 Subject: [PATCH 2/8] Added documentation. --- docs/source/command_line.rst | 39 ++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 251919d..0d5d298 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -6,20 +6,22 @@ from the command line and output metadata to stdout. .. code-block:: shell - $ wavinfo [--ixml | --adm] INFILE + + $ wavinfo [[-i] | [--ixml | --adm]] INFILE + -By default, `wavinfo` will output a JSON dictionary for each file argument. Options ------- -Two option flags will change the behavior of the command: +By default, `wavinfo` will output a JSON dictionary for each file argument. + +Two option flags will change the behavior of the command in non-interactive +mode: ``--ixml`` - The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload - of each input wave file, or will emit an error message to stderr if iXML - metadata is not present. + The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata + payload of each input wave file, or will emit an error message to stderr if + iXML metadata is not present. ``--adm`` The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata @@ -28,6 +30,31 @@ Two option flags will change the behavior of the command: These options are mutually-exclusive, with `\-\-adm` taking precedence. +If ``-i`` is given, `wavinfo` will run in `interactive mode`_. + + +Interactive Mode +----------------- + +In interactive mode, `wavinfo` will present a command prompt which allows you +to query the files provided on the command line and explore the metadata tree +interactively. Each file on the command line is scanned and presented as a +tree of metadata records. + +Commands include: + +``ls`` + List the available metadata keys at the current level. + +``cd`` + Traverse to a metadata key in the current level (or enter `..` to go up + to the prevvious level). + +``bye`` + Exit to the shell. + +Type `help` or `?` at the prompt to get a full list of commands. + Example Output -------------- From 206962b2183605e6407de98c33d951a288541b38 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 25 Nov 2024 10:16:26 -0800 Subject: [PATCH 3/8] More documentation changes. --- data/share/man/man1/wavinfo.1 | 5 +++++ docs/source/command_line.rst | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/data/share/man/man1/wavinfo.1 b/data/share/man/man1/wavinfo.1 index 58fd7d6..9719979 100644 --- a/data/share/man/man1/wavinfo.1 +++ b/data/share/man/man1/wavinfo.1 @@ -3,6 +3,7 @@ wavinfo \- probe wave files for metadata .SH SYNOPSIS .SY wavinfo +.I "[\-i]" .I "[\-\-adm]" .I "[\-\-ixml]" .I FILE ... @@ -24,6 +25,10 @@ Output any iXML metdata in .BR FILE . .IP "\-h, \-\-help" Print brief help. +.IP "\-i" +Enter +.I "interactive mode" +and browse metadata in FILE with an interactive command prompt. .SH DETAILED DESCRIPTION .B wavinfo collects metadata according to different diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 0d5d298..78a1a27 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -9,12 +9,14 @@ from the command line and output metadata to stdout. $ wavinfo [[-i] | [--ixml | --adm]] INFILE + - Options ------- By default, `wavinfo` will output a JSON dictionary for each file argument. +``-i`` + `wavinfo` will run in `interactive mode`_. + Two option flags will change the behavior of the command in non-interactive mode: @@ -28,9 +30,8 @@ mode: payload of each input wave file, or will emit an error message to stderr if ADM XML metadata is not present. -These options are mutually-exclusive, with `\-\-adm` taking precedence. - -If ``-i`` is given, `wavinfo` will run in `interactive mode`_. +These options are mutually-exclusive, with `\-\-adm` taking precedence. The +``--ixml`` and ``--adm`` flags futher take precedence over ``-i``. Interactive Mode From ffc0c48af7a762efc16c9260658f512611548fd0 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 25 Nov 2024 10:18:24 -0800 Subject: [PATCH 4/8] A small change to report umids as binary data --- wavinfo/wave_bext_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wavinfo/wave_bext_reader.py b/wavinfo/wave_bext_reader.py index e65925f..72200dc 100644 --- a/wavinfo/wave_bext_reader.py +++ b/wavinfo/wave_bext_reader.py @@ -89,7 +89,7 @@ class WavBextReader: # umid_str = umid_parsed.basic_umid_to_str() # else: - umid_str = None + # umid_str = None return {'description': self.description, 'originator': self.originator, @@ -98,7 +98,7 @@ class WavBextReader: 'originator_time': self.originator_time, 'time_reference': self.time_reference, 'version': self.version, - 'umid': umid_str, + 'umid': self.umid, 'coding_history': self.coding_history, 'loudness_value': self.loudness_value, 'loudness_range': self.loudness_range, From 36e4a02ab8262c3ec9ff63349deffe5107c4027f Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 25 Nov 2024 10:27:25 -0800 Subject: [PATCH 5/8] Documenation of base64 output --- docs/source/command_line.rst | 122 ++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 78a1a27..f392510 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -60,63 +60,81 @@ Type `help` or `?` at the prompt to get a full list of commands. Example Output -------------- +.. attention:: + + Metadata fields containing binary data, such as the Broadcast-WAV UMID, will + be included in the JSON output as a base-64 encoded string, preceded by the + marker "base64:". + .. code-block:: javascript - { - "filename": "tests/test_files/sounddevices/A101_1.WAV", - "run_date": "2022-11-26T17:56:38.342935", - "application": "wavinfo 2.1.0", - "scopes": { - "fmt": { - "audio_format": 1, - "channel_count": 2, - "sample_rate": 48000, - "byte_rate": 288000, - "block_align": 6, - "bits_per_sample": 24 + { + "filename": "../tests/test_files/nuendo/wavinfo Test Project - Audio - 1OA.wav", + "run_date": "2024-11-25T10:26:11.280053", + "application": "wavinfo 3.0.0", + "scopes": { + "fmt": { + "audio_format": 65534, + "channel_count": 4, + "sample_rate": 48000, + "byte_rate": 576000, + "block_align": 12, + "bits_per_sample": 24 + }, + "data": { + "byte_count": 576000, + "frame_count": 48000 + }, + "ixml": { + "track_list": [ + { + "channel_index": "1", + "interleave_index": "1", + "name": "", + "function": "ACN0-FOA" }, - "data": { - "byte_count": 1441434, - "frame_count": 240239 + { + "channel_index": "2", + "interleave_index": "2", + "name": "", + "function": "ACN1-FOA" }, - "ixml": { - "track_list": [ - { - "channel_index": "1", - "interleave_index": "1", - "name": "MKH516 A", - "function": "" - }, - { - "channel_index": "2", - "interleave_index": "2", - "name": "Boom", - "function": "" - } - ], - "project": "BMH", - "scene": "A101", - "take": "1", - "tape": "18Y12M31", - "family_uid": "USSDVGR1112089007124001008206300", - "family_name": null + { + "channel_index": "3", + "interleave_index": "3", + "name": "", + "function": "ACN2-FOA" }, - "bext": { - "description": "sSPEED=023.976-ND\r\nsTAKE=1\r\nsUBITS=$12311801\r\nsSWVER=2.67\r\nsPROJECT=BMH\r\nsSCENE=A101\r\nsFILENAME=A101_1.WAV\r\nsTAPE=18Y12M31\r\nsTRK1=MKH516 A\r\nsTRK2=Boom\r\nsNOTE=\r\n", - "originator": "Sound Dev: 702T S#GR1112089007", - "originator_ref": "USSDVGR1112089007124001008206301", - "originator_date": "2018-12-31", - "originator_time": "12:40:00", - "time_reference": 2190940753, - "version": 1, - "umid": "0000000000000000000000000000000000000000000000000000000000000000", - "coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n", - "loudness_value": null, - "loudness_range": null, - "max_true_peak": null, - "max_momentary_loudness": null, - "max_shortterm_loudness": null + { + "channel_index": "4", + "interleave_index": "4", + "name": "", + "function": "ACN3-FOA" } + ], + "project": "wavinfo Test Project", + "scene": null, + "take": null, + "tape": null, + "family_uid": "E5DDE719B9484A758162FF7B652383A3", + "family_name": null + }, + "bext": { + "description": "wavinfo Test Project Nuendo output", + "originator": "Nuendo", + "originator_ref": "USJPHNNNNNNNNN202829RRRRRRRRR", + "originator_date": "2022-12-02", + "originator_time": "10:21:06", + "time_reference": 172800000, + "version": 2, + "umid": "base64:k/zr4qE4RiaXyd/fO7GuCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "coding_history": "A=PCM,F=48000,W=24,T=Nuendo\r\n", + "loudness_value": 327.67, + "loudness_range": 327.67, + "max_true_peak": 327.67, + "max_momentary_loudness": 327.67, + "max_shortterm_loudness": 327.67 } - } + } +} From ac37c14b3d88e5bbc8d3edb747c39b7898de7258 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 25 Nov 2024 10:30:02 -0800 Subject: [PATCH 6/8] flake8 --- wavinfo/__main__.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/wavinfo/__main__.py b/wavinfo/__main__.py index d144d68..82a0dbf 100644 --- a/wavinfo/__main__.py +++ b/wavinfo/__main__.py @@ -45,38 +45,38 @@ class MetaBrowser(Cmd): @staticmethod def print_value(collection, key): - val = collection[key] - if isinstance(val, int): - print(f" - {key}: {val}") - elif isinstance(val, str): - print(f" - {key}: \"{val}\"") - elif isinstance(val, dict): - print(f" - {key}: Dict ({len(val)} keys)") - elif isinstance(val, list): - print(f" - {key}: List ({len(val)} keys)") - elif isinstance(val, bytes): - print(f" - {key}: ({len(val)} bytes)") - elif val == None: - print(f" - {key}: (NO VALUE)") - else: - print(f" - {key}: Unknown") + val = collection[key] + if isinstance(val, int): + print(f" - {key}: {val}") + elif isinstance(val, str): + print(f" - {key}: \"{val}\"") + elif isinstance(val, dict): + print(f" - {key}: Dict ({len(val)} keys)") + elif isinstance(val, list): + print(f" - {key}: List ({len(val)} keys)") + elif isinstance(val, bytes): + print(f" - {key}: ({len(val)} bytes)") + elif val is None: + print(f" - {key}: (NO VALUE)") + else: + print(f" - {key}: Unknown") def do_ls(self, _): 'List items at the current node: LS' - root = self.cwd + root = self.cwd if isinstance(root, list): - print(f"List:") + print("List:") for i in range(len(root)): self.print_value(root, i) elif isinstance(root, dict): - print(f"Dictionary:") + print("Dictionary:") for key in root: self.print_value(root, key) else: - print(f"Cannot print node, is not a list or dictionary.") + print("Cannot print node, is not a list or dictionary.") def do_cd(self, args): 'Switch to a different node: CD node-name | ".."' @@ -174,6 +174,5 @@ def main(): cli.cmdloop() - if __name__ == "__main__": main() From c13b07e4a30c5510896fed558ad7b366701cd9e8 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 25 Nov 2024 10:33:07 -0800 Subject: [PATCH 7/8] typing fix for python 3.8/3.9 --- wavinfo/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wavinfo/__main__.py b/wavinfo/__main__.py index 82a0dbf..3e5ad9f 100644 --- a/wavinfo/__main__.py +++ b/wavinfo/__main__.py @@ -9,7 +9,7 @@ from enum import Enum from base64 import b64encode from cmd import Cmd from shlex import split -from typing import List, Dict +from typing import List, Dict, Union class MyJSONEncoder(json.JSONEncoder): @@ -29,7 +29,7 @@ class MissingDataError(RuntimeError): class MetaBrowser(Cmd): prompt = "(wavinfo) " - metadata: List | Dict + metadata: Union[List, Dict] path: List[str] = [] @property From 2830cb87a4c766ef28a07a05af1e6d702ecd5a20 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Mon, 25 Nov 2024 11:15:16 -0800 Subject: [PATCH 8/8] flake8 --- wavinfo/__main__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/wavinfo/__main__.py b/wavinfo/__main__.py index c7e1d59..fe26f3f 100644 --- a/wavinfo/__main__.py +++ b/wavinfo/__main__.py @@ -142,7 +142,6 @@ def main(): interactive_dict = [] - # if options.install_manpages: # print("Installing manpages...") # print(f"Docfiles at {__file__}")