Merge pull request #37 from iluvcapra/feature-interactive

Feature: interactive shell
This commit is contained in:
Jamie Hardt
2024-11-25 11:17:20 -08:00
committed by GitHub
4 changed files with 213 additions and 63 deletions

View File

@@ -6,88 +6,134 @@ from the command line and output metadata to stdout.
.. code-block:: shell .. 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 Options
------- -------
Two option flags will change the behavior of the command: 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:
``--ixml`` ``--ixml``
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata
of each input wave file, or will emit an error message to stderr if iXML payload of each input wave file, or will emit an error message to stderr if
metadata is not present. iXML metadata is not present.
``--adm`` ``--adm``
The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata
payload of each input wave file, or will emit an error message to stderr if payload of each input wave file, or will emit an error message to stderr if
ADM XML metadata is not present. ADM XML metadata is not present.
These options are mutually-exclusive, with `\-\-adm` taking precedence. These options are mutually-exclusive, with `\-\-adm` taking precedence. The
``--ixml`` and ``--adm`` flags futher take precedence over ``-i``.
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 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 .. code-block:: javascript
{ {
"filename": "tests/test_files/sounddevices/A101_1.WAV", "filename": "../tests/test_files/nuendo/wavinfo Test Project - Audio - 1OA.wav",
"run_date": "2022-11-26T17:56:38.342935", "run_date": "2024-11-25T10:26:11.280053",
"application": "wavinfo 2.1.0", "application": "wavinfo 3.0.0",
"scopes": { "scopes": {
"fmt": { "fmt": {
"audio_format": 1, "audio_format": 65534,
"channel_count": 2, "channel_count": 4,
"sample_rate": 48000, "sample_rate": 48000,
"byte_rate": 288000, "byte_rate": 576000,
"block_align": 6, "block_align": 12,
"bits_per_sample": 24 "bits_per_sample": 24
}, },
"data": { "data": {
"byte_count": 1441434, "byte_count": 576000,
"frame_count": 240239 "frame_count": 48000
}, },
"ixml": { "ixml": {
"track_list": [ "track_list": [
{ {
"channel_index": "1", "channel_index": "1",
"interleave_index": "1", "interleave_index": "1",
"name": "MKH516 A", "name": "",
"function": "" "function": "ACN0-FOA"
}, },
{ {
"channel_index": "2", "channel_index": "2",
"interleave_index": "2", "interleave_index": "2",
"name": "Boom", "name": "",
"function": "" "function": "ACN1-FOA"
},
{
"channel_index": "3",
"interleave_index": "3",
"name": "",
"function": "ACN2-FOA"
},
{
"channel_index": "4",
"interleave_index": "4",
"name": "",
"function": "ACN3-FOA"
} }
], ],
"project": "BMH", "project": "wavinfo Test Project",
"scene": "A101", "scene": null,
"take": "1", "take": null,
"tape": "18Y12M31", "tape": null,
"family_uid": "USSDVGR1112089007124001008206300", "family_uid": "E5DDE719B9484A758162FF7B652383A3",
"family_name": null "family_name": null
}, },
"bext": { "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", "description": "wavinfo Test Project Nuendo output",
"originator": "Sound Dev: 702T S#GR1112089007", "originator": "Nuendo",
"originator_ref": "USSDVGR1112089007124001008206301", "originator_ref": "USJPHNNNNNNNNN202829RRRRRRRRR",
"originator_date": "2018-12-31", "originator_date": "2022-12-02",
"originator_time": "12:40:00", "originator_time": "10:21:06",
"time_reference": 2190940753, "time_reference": 172800000,
"version": 1, "version": 2,
"umid": "0000000000000000000000000000000000000000000000000000000000000000", "umid": "base64:k/zr4qE4RiaXyd/fO7GuCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n", "coding_history": "A=PCM,F=48000,W=24,T=Nuendo\r\n",
"loudness_value": null, "loudness_value": 327.67,
"loudness_range": null, "loudness_range": 327.67,
"max_true_peak": null, "max_true_peak": 327.67,
"max_momentary_loudness": null, "max_momentary_loudness": 327.67,
"max_shortterm_loudness": null "max_shortterm_loudness": 327.67
} }
} }
} }

View File

@@ -8,6 +8,9 @@ import json
from enum import Enum from enum import Enum
import importlib.metadata import importlib.metadata
from base64 import b64encode from base64 import b64encode
from cmd import Cmd
from shlex import split
from typing import List, Dict, Union
class MyJSONEncoder(json.JSONEncoder): class MyJSONEncoder(json.JSONEncoder):
@@ -24,6 +27,85 @@ class MissingDataError(RuntimeError):
pass pass
class MetaBrowser(Cmd):
prompt = "(wavinfo) "
metadata: Union[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 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
if isinstance(root, list):
print("List:")
for i in range(len(root)):
self.print_value(root, i)
elif isinstance(root, dict):
print("Dictionary:")
for key in root:
self.print_value(root, key)
else:
print("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(): def main():
version = importlib.metadata.version('wavinfo') version = importlib.metadata.version('wavinfo')
manpath = os.path.dirname(__file__) + "/man" manpath = os.path.dirname(__file__) + "/man"
@@ -51,8 +133,15 @@ def main():
default=False, default=False,
action='store_true') 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) (options, args) = parser.parse_args(sys.argv)
interactive_dict = []
# if options.install_manpages: # if options.install_manpages:
# print("Installing manpages...") # print("Installing manpages...")
# print(f"Docfiles at {__file__}") # print(f"Docfiles at {__file__}")
@@ -98,7 +187,12 @@ def main():
ret_dict['scopes'][scope][name] = value 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: except MissingDataError as e:
print("MissingDataError: Missing metadata (%s) in file %s" % print("MissingDataError: Missing metadata (%s) in file %s" %
(e, arg), file=sys.stderr) (e, arg), file=sys.stderr)
@@ -106,6 +200,11 @@ def main():
except Exception as e: except Exception as e:
raise e raise e
if len(interactive_dict) > 0:
cli = MetaBrowser()
cli.metadata = interactive_dict
cli.cmdloop()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -3,6 +3,7 @@
wavinfo \- probe wave files for metadata wavinfo \- probe wave files for metadata
.SH SYNOPSIS .SH SYNOPSIS
.SY wavinfo .SY wavinfo
.I "[\-i]"
.I "[\-\-adm]" .I "[\-\-adm]"
.I "[\-\-ixml]" .I "[\-\-ixml]"
.I FILE ... .I FILE ...
@@ -24,6 +25,10 @@ Output any iXML metdata in
.BR FILE . .BR FILE .
.IP "\-h, \-\-help" .IP "\-h, \-\-help"
Print brief help. Print brief help.
.IP "\-i"
Enter
.I "interactive mode"
and browse metadata in FILE with an interactive command prompt.
.SH DETAILED DESCRIPTION .SH DETAILED DESCRIPTION
.B wavinfo .B wavinfo
collects metadata according to different collects metadata according to different

View File

@@ -89,7 +89,7 @@ class WavBextReader:
# umid_str = umid_parsed.basic_umid_to_str() # umid_str = umid_parsed.basic_umid_to_str()
# else: # else:
umid_str = None # umid_str = None
return {'description': self.description, return {'description': self.description,
'originator': self.originator, 'originator': self.originator,
@@ -98,7 +98,7 @@ class WavBextReader:
'originator_time': self.originator_time, 'originator_time': self.originator_time,
'time_reference': self.time_reference, 'time_reference': self.time_reference,
'version': self.version, 'version': self.version,
'umid': umid_str, 'umid': self.umid,
'coding_history': self.coding_history, 'coding_history': self.coding_history,
'loudness_value': self.loudness_value, 'loudness_value': self.loudness_value,
'loudness_range': self.loudness_range, 'loudness_range': self.loudness_range,