Skip to content

Commit 94563f6

Browse files
authored
Merge pull request #37 from iluvcapra/feature-interactive
Feature: interactive shell
2 parents 1d499d9 + 2830cb8 commit 94563f6

File tree

4 files changed

+213
-63
lines changed

4 files changed

+213
-63
lines changed

docs/source/command_line.rst

Lines changed: 106 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,89 +6,135 @@ from the command line and output metadata to stdout.
66

77
.. code-block:: shell
88
9-
$ wavinfo [--ixml | --adm] INFILE +
10-
11-
By default, `wavinfo` will output a JSON dictionary for each file argument.
9+
$ wavinfo [[-i] | [--ixml | --adm]] INFILE +
1210
1311
1412
Options
1513
-------
1614

17-
Two option flags will change the behavior of the command:
15+
By default, `wavinfo` will output a JSON dictionary for each file argument.
16+
17+
``-i``
18+
`wavinfo` will run in `interactive mode`_.
19+
20+
Two option flags will change the behavior of the command in non-interactive
21+
mode:
1822

1923
``--ixml``
20-
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata payload
21-
of each input wave file, or will emit an error message to stderr if iXML
22-
metadata is not present.
24+
The *\-\-ixml* flag will cause `wavinfo` to output the iXML metadata
25+
payload of each input wave file, or will emit an error message to stderr if
26+
iXML metadata is not present.
2327

2428
``--adm``
2529
The *\-\-adm* flag will cause `wavinfo` to output the ADM XML metadata
2630
payload of each input wave file, or will emit an error message to stderr if
2731
ADM XML metadata is not present.
2832

29-
These options are mutually-exclusive, with `\-\-adm` taking precedence.
33+
These options are mutually-exclusive, with `\-\-adm` taking precedence. The
34+
``--ixml`` and ``--adm`` flags futher take precedence over ``-i``.
35+
36+
37+
Interactive Mode
38+
-----------------
39+
40+
In interactive mode, `wavinfo` will present a command prompt which allows you
41+
to query the files provided on the command line and explore the metadata tree
42+
interactively. Each file on the command line is scanned and presented as a
43+
tree of metadata records.
44+
45+
Commands include:
46+
47+
``ls``
48+
List the available metadata keys at the current level.
49+
50+
``cd``
51+
Traverse to a metadata key in the current level (or enter `..` to go up
52+
to the prevvious level).
53+
54+
``bye``
55+
Exit to the shell.
56+
57+
Type `help` or `?` at the prompt to get a full list of commands.
3058

3159

3260
Example Output
3361
--------------
3462

63+
.. attention::
64+
65+
Metadata fields containing binary data, such as the Broadcast-WAV UMID, will
66+
be included in the JSON output as a base-64 encoded string, preceded by the
67+
marker "base64:".
68+
3569
.. code-block:: javascript
3670
37-
{
38-
"filename": "tests/test_files/sounddevices/A101_1.WAV",
39-
"run_date": "2022-11-26T17:56:38.342935",
40-
"application": "wavinfo 2.1.0",
41-
"scopes": {
42-
"fmt": {
43-
"audio_format": 1,
44-
"channel_count": 2,
45-
"sample_rate": 48000,
46-
"byte_rate": 288000,
47-
"block_align": 6,
48-
"bits_per_sample": 24
71+
{
72+
"filename": "../tests/test_files/nuendo/wavinfo Test Project - Audio - 1OA.wav",
73+
"run_date": "2024-11-25T10:26:11.280053",
74+
"application": "wavinfo 3.0.0",
75+
"scopes": {
76+
"fmt": {
77+
"audio_format": 65534,
78+
"channel_count": 4,
79+
"sample_rate": 48000,
80+
"byte_rate": 576000,
81+
"block_align": 12,
82+
"bits_per_sample": 24
83+
},
84+
"data": {
85+
"byte_count": 576000,
86+
"frame_count": 48000
87+
},
88+
"ixml": {
89+
"track_list": [
90+
{
91+
"channel_index": "1",
92+
"interleave_index": "1",
93+
"name": "",
94+
"function": "ACN0-FOA"
4995
},
50-
"data": {
51-
"byte_count": 1441434,
52-
"frame_count": 240239
96+
{
97+
"channel_index": "2",
98+
"interleave_index": "2",
99+
"name": "",
100+
"function": "ACN1-FOA"
53101
},
54-
"ixml": {
55-
"track_list": [
56-
{
57-
"channel_index": "1",
58-
"interleave_index": "1",
59-
"name": "MKH516 A",
60-
"function": ""
61-
},
62-
{
63-
"channel_index": "2",
64-
"interleave_index": "2",
65-
"name": "Boom",
66-
"function": ""
67-
}
68-
],
69-
"project": "BMH",
70-
"scene": "A101",
71-
"take": "1",
72-
"tape": "18Y12M31",
73-
"family_uid": "USSDVGR1112089007124001008206300",
74-
"family_name": null
102+
{
103+
"channel_index": "3",
104+
"interleave_index": "3",
105+
"name": "",
106+
"function": "ACN2-FOA"
75107
},
76-
"bext": {
77-
"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",
78-
"originator": "Sound Dev: 702T S#GR1112089007",
79-
"originator_ref": "USSDVGR1112089007124001008206301",
80-
"originator_date": "2018-12-31",
81-
"originator_time": "12:40:00",
82-
"time_reference": 2190940753,
83-
"version": 1,
84-
"umid": "0000000000000000000000000000000000000000000000000000000000000000",
85-
"coding_history": "A=PCM,F=48000,W=24,M=stereo,R=48000,T=2 Ch\r\n",
86-
"loudness_value": null,
87-
"loudness_range": null,
88-
"max_true_peak": null,
89-
"max_momentary_loudness": null,
90-
"max_shortterm_loudness": null
108+
{
109+
"channel_index": "4",
110+
"interleave_index": "4",
111+
"name": "",
112+
"function": "ACN3-FOA"
91113
}
114+
],
115+
"project": "wavinfo Test Project",
116+
"scene": null,
117+
"take": null,
118+
"tape": null,
119+
"family_uid": "E5DDE719B9484A758162FF7B652383A3",
120+
"family_name": null
121+
},
122+
"bext": {
123+
"description": "wavinfo Test Project Nuendo output",
124+
"originator": "Nuendo",
125+
"originator_ref": "USJPHNNNNNNNNN202829RRRRRRRRR",
126+
"originator_date": "2022-12-02",
127+
"originator_time": "10:21:06",
128+
"time_reference": 172800000,
129+
"version": 2,
130+
"umid": "base64:k/zr4qE4RiaXyd/fO7GuCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
131+
"coding_history": "A=PCM,F=48000,W=24,T=Nuendo\r\n",
132+
"loudness_value": 327.67,
133+
"loudness_range": 327.67,
134+
"max_true_peak": 327.67,
135+
"max_momentary_loudness": 327.67,
136+
"max_shortterm_loudness": 327.67
92137
}
93-
}
138+
}
139+
}
94140

wavinfo/__main__.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from enum import Enum
99
import importlib.metadata
1010
from base64 import b64encode
11+
from cmd import Cmd
12+
from shlex import split
13+
from typing import List, Dict, Union
1114

1215

1316
class MyJSONEncoder(json.JSONEncoder):
@@ -24,6 +27,85 @@ class MissingDataError(RuntimeError):
2427
pass
2528

2629

30+
class MetaBrowser(Cmd):
31+
prompt = "(wavinfo) "
32+
33+
metadata: Union[List, Dict]
34+
path: List[str] = []
35+
36+
@property
37+
def cwd(self):
38+
root: List | Dict = self.metadata
39+
for key in self.path:
40+
if isinstance(root, list):
41+
root = root[int(key)]
42+
else:
43+
root = root[key]
44+
45+
return root
46+
47+
@staticmethod
48+
def print_value(collection, key):
49+
val = collection[key]
50+
if isinstance(val, int):
51+
print(f" - {key}: {val}")
52+
elif isinstance(val, str):
53+
print(f" - {key}: \"{val}\"")
54+
elif isinstance(val, dict):
55+
print(f" - {key}: Dict ({len(val)} keys)")
56+
elif isinstance(val, list):
57+
print(f" - {key}: List ({len(val)} keys)")
58+
elif isinstance(val, bytes):
59+
print(f" - {key}: ({len(val)} bytes)")
60+
elif val is None:
61+
print(f" - {key}: (NO VALUE)")
62+
else:
63+
print(f" - {key}: Unknown")
64+
65+
def do_ls(self, _):
66+
'List items at the current node: LS'
67+
root = self.cwd
68+
69+
if isinstance(root, list):
70+
print("List:")
71+
for i in range(len(root)):
72+
self.print_value(root, i)
73+
74+
elif isinstance(root, dict):
75+
print("Dictionary:")
76+
for key in root:
77+
self.print_value(root, key)
78+
79+
else:
80+
print("Cannot print node, is not a list or dictionary.")
81+
82+
def do_cd(self, args):
83+
'Switch to a different node: CD node-name | ".."'
84+
argv = split(args)
85+
if argv[0] == "..":
86+
self.path = self.path[0:-1]
87+
else:
88+
if isinstance(self.cwd, list):
89+
if int(argv[0]) < len(self.cwd):
90+
self.path = self.path + [argv[0]]
91+
else:
92+
print(f"Index {argv[0]} does not exist")
93+
elif isinstance(self.cwd, dict):
94+
if argv[0] in self.cwd.keys():
95+
self.path = self.path + [argv[0]]
96+
else:
97+
print(f"Key \"{argv[0]}\" does not exist")
98+
99+
if len(self.path) > 0:
100+
self.prompt = "(" + "/".join(self.path) + ") "
101+
else:
102+
self.prompt = "(wavinfo) "
103+
104+
def do_bye(self, _):
105+
'Exit the interactive browser: BYE'
106+
return True
107+
108+
27109
def main():
28110
version = importlib.metadata.version('wavinfo')
29111
manpath = os.path.dirname(__file__) + "/man"
@@ -51,8 +133,15 @@ def main():
51133
default=False,
52134
action='store_true')
53135

136+
parser.add_option('-i',
137+
help='Read metadata with an interactive prompt',
138+
default=False,
139+
action='store_true')
140+
54141
(options, args) = parser.parse_args(sys.argv)
55142

143+
interactive_dict = []
144+
56145
# if options.install_manpages:
57146
# print("Installing manpages...")
58147
# print(f"Docfiles at {__file__}")
@@ -98,14 +187,24 @@ def main():
98187

99188
ret_dict['scopes'][scope][name] = value
100189

101-
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout, indent=2)
190+
if options.i:
191+
interactive_dict.append(ret_dict)
192+
else:
193+
json.dump(ret_dict, cls=MyJSONEncoder, fp=sys.stdout,
194+
indent=2)
195+
102196
except MissingDataError as e:
103197
print("MissingDataError: Missing metadata (%s) in file %s" %
104198
(e, arg), file=sys.stderr)
105199
continue
106200
except Exception as e:
107201
raise e
108202

203+
if len(interactive_dict) > 0:
204+
cli = MetaBrowser()
205+
cli.metadata = interactive_dict
206+
cli.cmdloop()
207+
109208

110209
if __name__ == "__main__":
111210
main()

wavinfo/man/man1/wavinfo.1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
wavinfo \- probe wave files for metadata
44
.SH SYNOPSIS
55
.SY wavinfo
6+
.I "[\-i]"
67
.I "[\-\-adm]"
78
.I "[\-\-ixml]"
89
.I FILE ...
@@ -24,6 +25,10 @@ Output any iXML metdata in
2425
.BR FILE .
2526
.IP "\-h, \-\-help"
2627
Print brief help.
28+
.IP "\-i"
29+
Enter
30+
.I "interactive mode"
31+
and browse metadata in FILE with an interactive command prompt.
2732
.SH DETAILED DESCRIPTION
2833
.B wavinfo
2934
collects metadata according to different

wavinfo/wave_bext_reader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def to_dict(self):
8989
# umid_str = umid_parsed.basic_umid_to_str()
9090
# else:
9191

92-
umid_str = None
92+
# umid_str = None
9393

9494
return {'description': self.description,
9595
'originator': self.originator,
@@ -98,7 +98,7 @@ def to_dict(self):
9898
'originator_time': self.originator_time,
9999
'time_reference': self.time_reference,
100100
'version': self.version,
101-
'umid': umid_str,
101+
'umid': self.umid,
102102
'coding_history': self.coding_history,
103103
'loudness_value': self.loudness_value,
104104
'loudness_range': self.loudness_range,

0 commit comments

Comments
 (0)